A deployable markdown editor that connects with your self hosted files and lets you edit in a beautiful interface

Implement plan items: last repo persistence, remove folder selection, filter empty folders, separate repo selection URL

Backend changes:
- Add last_repo field to UserResponse in auth.go:27
- Update GetCurrentUser to include last_repo in response (auth.go:238)
- Update router to use POST /api/user/repo instead of /api/auth/last-repo (router.go:63)
- Implement empty folder filtering in github.go ListFiles function
- Add hasMatchingFiles helper to recursively check for matching files

Frontend changes:
- Simplify SetupWizard: remove 2-step wizard, default to root folder
- Add API call to save last_repo when completing setup
- Create new /select-repo page with SetupWizard component
- Update DashboardApp to check for last_repo from user data or URL params
- Redirect to /select-repo if no repo is configured
- Update 'Change repository' button to link to /select-repo
- Add last_repo field to User type in api.ts

This implements items 1-4 from the plan:
1. Save & Load Last Repo - Backend returns last_repo, frontend saves it via API
2. Remove Folder Selection - Setup wizard now single-step, defaults to root
3. Filter Empty Folders - Backend only returns folders with markdown files
4. Separate Repo Selection URL - New /select-repo page for repo selection

+208 -275
+2
backend/internal/api/handlers/auth.go
··· 30 Username string `json:"username"` 31 AvatarURL string `json:"avatar_url"` 32 Provider string `json:"provider"` 33 } 34 35 // min returns the minimum of two integers ··· 235 Username: user.Username, 236 AvatarURL: user.AvatarURL, 237 Provider: "github", 238 } 239 240 w.Header().Set("Content-Type", "application/json")
··· 30 Username string `json:"username"` 31 AvatarURL string `json:"avatar_url"` 32 Provider string `json:"provider"` 33 + LastRepo string `json:"last_repo"` 34 } 35 36 // min returns the minimum of two integers ··· 236 Username: user.Username, 237 AvatarURL: user.AvatarURL, 238 Provider: "github", 239 + LastRepo: user.LastRepo, 240 } 241 242 w.Header().Set("Content-Type", "application/json")
+1 -1
backend/internal/api/router.go
··· 60 61 r.Get("/api/auth/user", authHandler.GetCurrentUser) 62 r.Post("/api/auth/logout", authHandler.Logout) 63 - r.Post("/api/auth/last-repo", authHandler.UpdateLastRepo) 64 65 // Repository routes 66 r.Get("/api/repos", repoHandler.ListRepositories)
··· 60 61 r.Get("/api/auth/user", authHandler.GetCurrentUser) 62 r.Post("/api/auth/logout", authHandler.Logout) 63 + r.Post("/api/user/repo", authHandler.UpdateLastRepo) 64 65 // Repository routes 66 r.Get("/api/repos", repoHandler.ListRepositories)
+32
backend/internal/connectors/github.go
··· 149 continue 150 } 151 node.Children = subNode.Children 152 } 153 154 root.Children = append(root.Children, node) 155 } 156 157 return root, nil 158 } 159 160 // GetFileContent retrieves the content of a file
··· 149 continue 150 } 151 node.Children = subNode.Children 152 + 153 + // Skip empty directories (directories with no matching files) 154 + if !hasMatchingFiles(&node, extensions) { 155 + continue 156 + } 157 } 158 159 root.Children = append(root.Children, node) 160 } 161 162 return root, nil 163 + } 164 + 165 + // hasMatchingFiles checks if a directory node contains any files (recursively) 166 + func hasMatchingFiles(node *FileNode, extensions []string) bool { 167 + if node.Type == "file" { 168 + // If extensions are specified, check if file matches 169 + if len(extensions) > 0 { 170 + ext := strings.TrimPrefix(filepath.Ext(node.Name), ".") 171 + for _, allowedExt := range extensions { 172 + if ext == allowedExt { 173 + return true 174 + } 175 + } 176 + return false 177 + } 178 + return true 179 + } 180 + 181 + if node.Type == "dir" { 182 + for _, child := range node.Children { 183 + if hasMatchingFiles(&child, extensions) { 184 + return true 185 + } 186 + } 187 + } 188 + 189 + return false 190 } 191 192 // GetFileContent retrieves the content of a file
+36 -10
frontend/src/components/dashboard/DashboardApp.tsx
··· 1 - import { useState } from 'react'; 2 import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 import { Toaster } from 'sonner'; 4 import { Header } from '../layout/Header'; 5 - import { SetupWizard } from './SetupWizard'; 6 import { FileTree } from './FileTree'; 7 import { EditorContainer } from '../editor/EditorContainer'; 8 import { useFiles } from '../../lib/hooks/useRepos'; ··· 46 const [selectedFile, setSelectedFile] = useState<string | null>(null); 47 const { data: user, isLoading: userLoading } = useCurrentUser(); 48 49 const { data: filesData, isLoading: filesLoading } = useFiles( 50 repoConfig?.owner || '', 51 repoConfig?.repo || '', ··· 70 return null; 71 } 72 73 - // Show setup wizard if no repository is configured 74 if (!repoConfig) { 75 return ( 76 - <SetupWizard 77 - onComplete={(config) => { 78 - setRepoConfig(config); 79 - // TODO: Save config to database/localStorage 80 - }} 81 - /> 82 ); 83 } 84 ··· 102 )} 103 </div> 104 <button 105 - onClick={() => setRepoConfig(null)} 106 className="mt-2 text-xs text-gray-600 hover:text-gray-900 underline" 107 > 108 Change repository
··· 1 + import { useState, useEffect } from 'react'; 2 import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 import { Toaster } from 'sonner'; 4 import { Header } from '../layout/Header'; 5 import { FileTree } from './FileTree'; 6 import { EditorContainer } from '../editor/EditorContainer'; 7 import { useFiles } from '../../lib/hooks/useRepos'; ··· 45 const [selectedFile, setSelectedFile] = useState<string | null>(null); 46 const { data: user, isLoading: userLoading } = useCurrentUser(); 47 48 + // Check for last_repo or URL params and redirect if needed 49 + useEffect(() => { 50 + if (userLoading || !user) return; 51 + 52 + // Check URL params first 53 + const urlParams = new URLSearchParams(window.location.search); 54 + const repoParam = urlParams.get('repo'); 55 + 56 + if (repoParam) { 57 + // Parse repo from URL (format: owner/repo) 58 + const [owner, repo] = repoParam.split('/'); 59 + if (owner && repo) { 60 + setRepoConfig({ owner, repo, folder: '' }); 61 + return; 62 + } 63 + } 64 + 65 + // Check user's last_repo 66 + if (user.last_repo) { 67 + const [owner, repo] = user.last_repo.split('/'); 68 + if (owner && repo) { 69 + setRepoConfig({ owner, repo, folder: '' }); 70 + return; 71 + } 72 + } 73 + 74 + // No repo configured, redirect to select-repo page 75 + window.location.href = '/select-repo'; 76 + }, [user, userLoading]); 77 + 78 const { data: filesData, isLoading: filesLoading } = useFiles( 79 repoConfig?.owner || '', 80 repoConfig?.repo || '', ··· 99 return null; 100 } 101 102 + // Show loading while checking for repo config 103 if (!repoConfig) { 104 return ( 105 + <div className="min-h-screen bg-gray-50 flex items-center justify-center"> 106 + <Loading size="lg" text="Loading repository..." /> 107 + </div> 108 ); 109 } 110 ··· 128 )} 129 </div> 130 <button 131 + onClick={() => window.location.href = '/select-repo'} 132 className="mt-2 text-xs text-gray-600 hover:text-gray-900 underline" 133 > 134 Change repository
+116 -264
frontend/src/components/dashboard/SetupWizard.tsx
··· 1 - import { useState, useEffect } from 'react'; 2 import { toast } from 'sonner'; 3 import { Button } from '../ui/button'; 4 import { useRepositories } from '../../lib/hooks/useRepos'; 5 import { apiClient } from '../../lib/api'; 6 7 - interface SetupWizardProps { 8 - onComplete: (config: { 9 - owner: string; 10 - repo: string; 11 - folder: string; 12 - }) => void; 13 - } 14 - 15 - interface FolderItem { 16 - name: string; 17 - path: string; 18 - type: 'file' | 'dir'; 19 - } 20 - 21 - export function SetupWizard({ onComplete }: SetupWizardProps) { 22 - const [step, setStep] = useState(1); 23 const [selectedRepoFullName, setSelectedRepoFullName] = useState(''); 24 - const [folder, setFolder] = useState(''); 25 - const [folders, setFolders] = useState<FolderItem[]>([]); 26 - const [loadingFolders, setLoadingFolders] = useState(false); 27 const [sortBy, setSortBy] = useState<'updated' | 'created' | 'name'>('updated'); 28 29 // Fetch repositories 30 const { data: repos, isLoading: reposLoading, error: reposError } = useRepositories(sortBy); ··· 32 // Parse owner and repo from selected repository 33 const [owner, repo] = selectedRepoFullName ? selectedRepoFullName.split('/') : ['', '']; 34 35 - // Fetch folders when we move to step 2 36 - useEffect(() => { 37 - if (step === 2 && owner && repo) { 38 - fetchFolders(''); 39 - } 40 - }, [step, owner, repo]); 41 42 - const fetchFolders = async (path: string) => { 43 - setLoadingFolders(true); 44 try { 45 - const response = await apiClient.get(`/api/repos/${owner}/${repo}/files`, { 46 - params: { path, ref: '' } 47 }); 48 - 49 - // Filter to only show directories 50 - const dirs = response.data.files 51 - .filter((item: FolderItem) => item.type === 'dir') 52 - .sort((a: FolderItem, b: FolderItem) => a.name.localeCompare(b.name)); 53 - 54 - setFolders(dirs); 55 } catch (error) { 56 - console.error('Failed to fetch folders:', error); 57 - toast.error('Failed to load folders', { 58 description: error instanceof Error ? error.message : 'Please try again', 59 }); 60 - setFolders([]); 61 - } finally { 62 - setLoadingFolders(false); 63 - } 64 - }; 65 - 66 - const handleNext = () => { 67 - if (step === 1 && selectedRepoFullName) { 68 - setStep(2); 69 - } else if (step === 2) { 70 - onComplete({ owner, repo, folder: folder || '' }); 71 } 72 - }; 73 - 74 - const handleBack = () => { 75 - setStep(step - 1); 76 }; 77 78 return ( ··· 91 </div> 92 93 <div className="bg-white rounded-lg border border-gray-200 shadow-sm p-8"> 94 - {/* Progress Steps */} 95 - <div className="flex items-center justify-center mb-8"> 96 - <div className="flex items-center"> 97 - <div 98 - className={`w-8 h-8 rounded-full flex items-center justify-center ${ 99 - step >= 1 100 - ? 'bg-gray-900 text-white' 101 - : 'bg-gray-200 text-gray-500' 102 - }`} 103 - > 104 - 1 105 - </div> 106 - <div 107 - className={`w-24 h-0.5 ${ 108 - step >= 2 ? 'bg-gray-900' : 'bg-gray-200' 109 - }`} 110 - ></div> 111 - <div 112 - className={`w-8 h-8 rounded-full flex items-center justify-center ${ 113 - step >= 2 114 - ? 'bg-gray-900 text-white' 115 - : 'bg-gray-200 text-gray-500' 116 - }`} 117 - > 118 - 2 119 - </div> 120 </div> 121 - </div> 122 123 - {/* Step 1: Repository Selection */} 124 - {step === 1 && ( 125 - <div className="space-y-6"> 126 - <div> 127 - <h2 className="text-xl font-bold text-gray-900 mb-4"> 128 - Select Your primary Repository 129 - </h2> 130 - <p className="text-sm text-gray-600 mb-6"> 131 - Choose the GitHub repository where your blog posts are stored. 132 - </p> 133 </div> 134 135 - <div className="space-y-4"> 136 - <div className="flex items-center justify-between mb-2"> 137 - <label className="block text-sm font-semibold text-gray-700"> 138 - Repository 139 - </label> 140 - <select 141 - value={sortBy} 142 - onChange={(e) => setSortBy(e.target.value as any)} 143 - className="text-xs px-2 py-1 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-900" 144 - > 145 - <option value="updated">Recently Updated</option> 146 - <option value="created">Recently Created</option> 147 - <option value="name">Name</option> 148 - </select> 149 - </div> 150 - 151 - {reposLoading ? ( 152 - <div className="w-full px-4 py-12 border border-gray-300 rounded-md bg-gray-50"> 153 - <div className="flex flex-col items-center justify-center"> 154 - <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mb-3"></div> 155 - <p className="text-sm text-gray-600">Loading your repositories...</p> 156 - </div> 157 - </div> 158 - ) : reposError ? ( 159 - <div className="w-full px-4 py-8 border border-red-300 rounded-md bg-red-50"> 160 - <div className="text-center"> 161 - <svg 162 - className="w-12 h-12 text-red-400 mx-auto mb-3" 163 - fill="none" 164 - viewBox="0 0 24 24" 165 - stroke="currentColor" 166 - > 167 - <path 168 - strokeLinecap="round" 169 - strokeLinejoin="round" 170 - strokeWidth={2} 171 - d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" 172 - /> 173 - </svg> 174 - <p className="text-sm text-red-800 font-semibold mb-1">Failed to load repositories</p> 175 - <p className="text-xs text-red-600"> 176 - Make sure you've granted MarkEdit access to your repositories. 177 - </p> 178 - </div> 179 - </div> 180 - ) : ( 181 - <> 182 - <select 183 - value={selectedRepoFullName} 184 - onChange={(e) => setSelectedRepoFullName(e.target.value)} 185 - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-base" 186 - > 187 - <option value="">Choose a repository...</option> 188 - {repos?.map((repo) => ( 189 - <option key={repo.id} value={repo.full_name}> 190 - {repo.full_name} 191 - {repo.private && ' 🔒'} 192 - </option> 193 - ))} 194 - </select> 195 - 196 - {repos && repos.length > 0 && ( 197 - <p className="text-xs text-gray-500"> 198 - {repos.length} {repos.length === 1 ? 'repository' : 'repositories'} found 199 - </p> 200 - )} 201 - 202 - {repos && repos.length === 0 && ( 203 - <div className="p-4 bg-amber-50 rounded-lg border border-amber-200"> 204 - <p className="text-sm text-amber-800"> 205 - No repositories found. Make sure you've authorized MarkEdit to access your GitHub repositories. 206 - </p> 207 - </div> 208 - )} 209 - </> 210 - )} 211 - 212 - {selectedRepoFullName && ( 213 - <div className="p-4 bg-gray-50 rounded-lg border border-gray-200"> 214 - <div className="text-sm text-gray-700"> 215 - <strong>Selected repository:</strong> 216 - <div className="mt-1 font-mono text-gray-900"> 217 - github.com/{selectedRepoFullName} 218 - </div> 219 - </div> 220 </div> 221 - )} 222 - </div> 223 - </div> 224 - )} 225 - 226 - {/* Step 2: Folder Configuration */} 227 - {step === 2 && ( 228 - <div className="space-y-6"> 229 - <div> 230 - <h2 className="text-xl font-bold text-gray-900 mb-4"> 231 - Configure Folder Path 232 - </h2> 233 - <p className="text-sm text-gray-600 mb-6"> 234 - Select which folder contains your blog posts, or leave empty to use the root directory. 235 - </p> 236 - </div> 237 - 238 - <div className="space-y-4"> 239 - <div> 240 - <label className="block text-sm font-semibold text-gray-700 mb-2"> 241 - Folder Path (optional) 242 - </label> 243 - 244 - {loadingFolders ? ( 245 - <div className="w-full px-4 py-8 border border-gray-300 rounded-md bg-gray-50"> 246 - <div className="flex flex-col items-center justify-center"> 247 - <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 mb-2"></div> 248 - <p className="text-xs text-gray-600">Loading folders...</p> 249 - </div> 250 - </div> 251 - ) : ( 252 - <> 253 - <select 254 - value={folder} 255 - onChange={(e) => setFolder(e.target.value)} 256 - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-base" 257 - > 258 - <option value="">📁 Root directory (all markdown files)</option> 259 - {folders.map((dir) => ( 260 - <option key={dir.path} value={dir.path}> 261 - 📁 {dir.path} 262 - </option> 263 - ))} 264 - </select> 265 - 266 - {folders.length === 0 && !loadingFolders && ( 267 - <p className="mt-2 text-xs text-gray-500"> 268 - No folders found in this repository. You can use the root directory. 269 - </p> 270 - )} 271 - 272 - {folders.length > 0 && ( 273 - <p className="mt-2 text-xs text-gray-500"> 274 - {folders.length} {folders.length === 1 ? 'folder' : 'folders'} found 275 - </p> 276 - )} 277 - </> 278 - )} 279 </div> 280 - 281 - <div className="p-4 bg-amber-50 rounded-lg border border-amber-200"> 282 - <div className="flex gap-3"> 283 <svg 284 - className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" 285 fill="none" 286 viewBox="0 0 24 24" 287 stroke="currentColor" ··· 290 strokeLinecap="round" 291 strokeLinejoin="round" 292 strokeWidth={2} 293 - d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" 294 /> 295 </svg> 296 - <div className="text-sm text-amber-800"> 297 - <strong className="block mb-1">Note:</strong> 298 - <p> 299 - MarkEdit will search for markdown files (.md, .mdx) in the selected folder and all its subfolders. 300 - </p> 301 - </div> 302 </div> 303 </div> 304 305 <div className="p-4 bg-gray-50 rounded-lg border border-gray-200"> 306 <div className="text-sm text-gray-700"> 307 - <strong>MarkEdit will monitor:</strong> 308 <div className="mt-1 font-mono text-gray-900"> 309 - {owner}/{repo}{folder ? `/${folder}` : ' (root)'} 310 </div> 311 </div> 312 </div> 313 </div> 314 </div> 315 - )} 316 317 {/* Navigation Buttons */} 318 - <div className="flex items-center justify-between mt-8 pt-6 border-t border-gray-200"> 319 <Button 320 - variant="outline" 321 - onClick={handleBack} 322 - disabled={step === 1} 323 - className={step === 1 ? 'invisible' : ''} 324 - > 325 - Back 326 - </Button> 327 - 328 - <Button 329 - onClick={handleNext} 330 - disabled={ 331 - (step === 1 && !selectedRepoFullName) || 332 - (step === 1 && reposLoading) 333 - } 334 className="bg-gray-900 hover:bg-gray-800 text-white" 335 > 336 - {step === 1 ? 'Next' : 'Complete Setup'} 337 </Button> 338 </div> 339 </div>
··· 1 + import { useState } from 'react'; 2 import { toast } from 'sonner'; 3 import { Button } from '../ui/button'; 4 import { useRepositories } from '../../lib/hooks/useRepos'; 5 import { apiClient } from '../../lib/api'; 6 7 + export function SetupWizard() { 8 const [selectedRepoFullName, setSelectedRepoFullName] = useState(''); 9 const [sortBy, setSortBy] = useState<'updated' | 'created' | 'name'>('updated'); 10 + const [isSubmitting, setIsSubmitting] = useState(false); 11 12 // Fetch repositories 13 const { data: repos, isLoading: reposLoading, error: reposError } = useRepositories(sortBy); ··· 15 // Parse owner and repo from selected repository 16 const [owner, repo] = selectedRepoFullName ? selectedRepoFullName.split('/') : ['', '']; 17 18 + const handleComplete = async () => { 19 + if (!selectedRepoFullName) return; 20 21 + setIsSubmitting(true); 22 try { 23 + // Save last_repo to user profile 24 + await apiClient.post('/api/user/repo', { 25 + last_repo: selectedRepoFullName, 26 }); 27 + 28 + // Redirect to dashboard with repo parameter 29 + window.location.href = `/dashboard?repo=${encodeURIComponent(selectedRepoFullName)}`; 30 } catch (error) { 31 + console.error('Failed to save repository:', error); 32 + toast.error('Failed to save repository', { 33 description: error instanceof Error ? error.message : 'Please try again', 34 }); 35 + setIsSubmitting(false); 36 } 37 }; 38 39 return ( ··· 52 </div> 53 54 <div className="bg-white rounded-lg border border-gray-200 shadow-sm p-8"> 55 + {/* Repository Selection */} 56 + <div className="space-y-6"> 57 + <div> 58 + <h2 className="text-xl font-bold text-gray-900 mb-4"> 59 + Select Your Repository 60 + </h2> 61 + <p className="text-sm text-gray-600 mb-6"> 62 + Choose the GitHub repository where your blog posts are stored. 63 + </p> 64 </div> 65 66 + <div className="space-y-4"> 67 + <div className="flex items-center justify-between mb-2"> 68 + <label className="block text-sm font-semibold text-gray-700"> 69 + Repository 70 + </label> 71 + <select 72 + value={sortBy} 73 + onChange={(e) => setSortBy(e.target.value as any)} 74 + className="text-xs px-2 py-1 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-900" 75 + > 76 + <option value="updated">Recently Updated</option> 77 + <option value="created">Recently Created</option> 78 + <option value="name">Name</option> 79 + </select> 80 </div> 81 82 + {reposLoading ? ( 83 + <div className="w-full px-4 py-12 border border-gray-300 rounded-md bg-gray-50"> 84 + <div className="flex flex-col items-center justify-center"> 85 + <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mb-3"></div> 86 + <p className="text-sm text-gray-600">Loading your repositories...</p> 87 </div> 88 </div> 89 + ) : reposError ? ( 90 + <div className="w-full px-4 py-8 border border-red-300 rounded-md bg-red-50"> 91 + <div className="text-center"> 92 <svg 93 + className="w-12 h-12 text-red-400 mx-auto mb-3" 94 fill="none" 95 viewBox="0 0 24 24" 96 stroke="currentColor" ··· 99 strokeLinecap="round" 100 strokeLinejoin="round" 101 strokeWidth={2} 102 + d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" 103 /> 104 </svg> 105 + <p className="text-sm text-red-800 font-semibold mb-1">Failed to load repositories</p> 106 + <p className="text-xs text-red-600"> 107 + Make sure you've granted MarkEdit access to your repositories. 108 + </p> 109 </div> 110 </div> 111 + ) : ( 112 + <> 113 + <select 114 + value={selectedRepoFullName} 115 + onChange={(e) => setSelectedRepoFullName(e.target.value)} 116 + className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-base" 117 + disabled={isSubmitting} 118 + > 119 + <option value="">Choose a repository...</option> 120 + {repos?.map((repo) => ( 121 + <option key={repo.id} value={repo.full_name}> 122 + {repo.full_name} 123 + {repo.private && ' 🔒'} 124 + </option> 125 + ))} 126 + </select> 127 128 + {repos && repos.length > 0 && ( 129 + <p className="text-xs text-gray-500"> 130 + {repos.length} {repos.length === 1 ? 'repository' : 'repositories'} found 131 + </p> 132 + )} 133 + 134 + {repos && repos.length === 0 && ( 135 + <div className="p-4 bg-amber-50 rounded-lg border border-amber-200"> 136 + <p className="text-sm text-amber-800"> 137 + No repositories found. Make sure you've authorized MarkEdit to access your GitHub repositories. 138 + </p> 139 + </div> 140 + )} 141 + </> 142 + )} 143 + 144 + {selectedRepoFullName && ( 145 <div className="p-4 bg-gray-50 rounded-lg border border-gray-200"> 146 <div className="text-sm text-gray-700"> 147 + <strong>Selected repository:</strong> 148 <div className="mt-1 font-mono text-gray-900"> 149 + github.com/{selectedRepoFullName} 150 </div> 151 </div> 152 </div> 153 + )} 154 + 155 + <div className="p-4 bg-blue-50 rounded-lg border border-blue-200"> 156 + <div className="flex gap-3"> 157 + <svg 158 + className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" 159 + fill="none" 160 + viewBox="0 0 24 24" 161 + stroke="currentColor" 162 + > 163 + <path 164 + strokeLinecap="round" 165 + strokeLinejoin="round" 166 + strokeWidth={2} 167 + d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" 168 + /> 169 + </svg> 170 + <div className="text-sm text-blue-800"> 171 + <strong className="block mb-1">Note:</strong> 172 + <p> 173 + MarkEdit will search for markdown files (.md, .mdx) in the entire repository. 174 + </p> 175 + </div> 176 + </div> 177 </div> 178 </div> 179 + </div> 180 181 {/* Navigation Buttons */} 182 + <div className="flex items-center justify-end mt-8 pt-6 border-t border-gray-200"> 183 <Button 184 + onClick={handleComplete} 185 + disabled={!selectedRepoFullName || reposLoading || isSubmitting} 186 className="bg-gray-900 hover:bg-gray-800 text-white" 187 > 188 + {isSubmitting ? 'Setting up...' : 'Complete Setup'} 189 </Button> 190 </div> 191 </div>
+1
frontend/src/lib/types/api.ts
··· 3 username: string; 4 avatar_url: string; 5 provider: string; 6 } 7 8 export interface Repository {
··· 3 username: string; 4 avatar_url: string; 5 provider: string; 6 + last_repo?: string; 7 } 8 9 export interface Repository {
+20
frontend/src/pages/select-repo.astro
···
··· 1 + --- 2 + import '../styles/globals.css'; 3 + import { SetupWizard } from '../components/dashboard/SetupWizard'; 4 + --- 5 + 6 + <html lang="en"> 7 + <head> 8 + <meta charset="utf-8" /> 9 + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 10 + <meta name="viewport" content="width=device-width" /> 11 + <meta name="generator" content={Astro.generator} /> 12 + <title>Select Repository - MarkEdit</title> 13 + <link rel="preconnect" href="https://fonts.googleapis.com"> 14 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 15 + <link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Crimson+Pro:wght@400;600&display=swap" rel="stylesheet"> 16 + </head> 17 + <body> 18 + <SetupWizard client:only="react" /> 19 + </body> 20 + </html>