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

Configure Feed

Select the types of activity you want to include in your feed.

Replace text inputs with dropdowns in setup wizard

Step 1: Repository Selection
- Fetch and display all user repositories in a dropdown
- Add sort options: recently updated, recently created, by name
- Show loading state while fetching repositories
- Show error state if fetch fails
- Display repository count

Step 2: Folder Selection
- Fetch all directories from selected repository
- Display folders in dropdown with emoji icons
- Default option: root directory (all markdown files)
- Show loading state while fetching folders
- Show helpful message if no folders found

Benefits:
- No typing errors - users select from actual repositories
- Better UX - see what actually exists
- Automatic validation - can only select real repos/folders

+171 -79
+171 -79
frontend/src/components/dashboard/SetupWizard.tsx
··· 1 - import { useState } from 'react'; 1 + import { useState, useEffect } from 'react'; 2 2 import { Button } from '../ui/button'; 3 + import { useRepositories } from '../../lib/hooks/useRepos'; 4 + import { apiClient } from '../../lib/api'; 3 5 4 6 interface SetupWizardProps { 5 7 onComplete: (config: { ··· 9 11 }) => void; 10 12 } 11 13 14 + interface FolderItem { 15 + name: string; 16 + path: string; 17 + type: 'file' | 'dir'; 18 + } 19 + 12 20 export function SetupWizard({ onComplete }: SetupWizardProps) { 13 21 const [step, setStep] = useState(1); 14 - const [owner, setOwner] = useState(''); 15 - const [repo, setRepo] = useState(''); 22 + const [selectedRepoFullName, setSelectedRepoFullName] = useState(''); 16 23 const [folder, setFolder] = useState(''); 24 + const [folders, setFolders] = useState<FolderItem[]>([]); 25 + const [loadingFolders, setLoadingFolders] = useState(false); 26 + const [sortBy, setSortBy] = useState<'updated' | 'created' | 'name'>('updated'); 27 + 28 + // Fetch repositories 29 + const { data: repos, isLoading: reposLoading, error: reposError } = useRepositories(sortBy); 30 + 31 + // Parse owner and repo from selected repository 32 + const [owner, repo] = selectedRepoFullName ? selectedRepoFullName.split('/') : ['', '']; 33 + 34 + // Fetch folders when we move to step 2 35 + useEffect(() => { 36 + if (step === 2 && owner && repo) { 37 + fetchFolders(''); 38 + } 39 + }, [step, owner, repo]); 40 + 41 + const fetchFolders = async (path: string) => { 42 + setLoadingFolders(true); 43 + try { 44 + const response = await apiClient.get(`/api/repos/${owner}/${repo}/files`, { 45 + params: { path, ref: '' } 46 + }); 47 + 48 + // Filter to only show directories 49 + const dirs = response.data.files 50 + .filter((item: FolderItem) => item.type === 'dir') 51 + .sort((a: FolderItem, b: FolderItem) => a.name.localeCompare(b.name)); 52 + 53 + setFolders(dirs); 54 + } catch (error) { 55 + console.error('Failed to fetch folders:', error); 56 + setFolders([]); 57 + } finally { 58 + setLoadingFolders(false); 59 + } 60 + }; 17 61 18 62 const handleNext = () => { 19 - if (step === 1 && owner && repo) { 63 + if (step === 1 && selectedRepoFullName) { 20 64 setStep(2); 21 65 } else if (step === 2) { 22 - onComplete({ owner, repo, folder: folder || '/' }); 66 + onComplete({ owner, repo, folder: folder || '' }); 23 67 } 24 68 }; 25 69 ··· 80 124 Select Your Repository 81 125 </h2> 82 126 <p className="text-sm text-gray-600 mb-6"> 83 - Enter the GitHub repository where your blog posts are stored. 84 - For example, if your repository is at{' '} 85 - <code className="bg-gray-100 px-1 py-0.5 rounded"> 86 - github.com/username/blog 87 - </code> 88 - , enter "username" as the owner and "blog" as the repository 89 - name. 127 + Choose the GitHub repository where your blog posts are stored. 90 128 </p> 91 129 </div> 92 130 93 131 <div className="space-y-4"> 94 - <div> 95 - <label className="block text-sm font-semibold text-gray-700 mb-2"> 96 - Repository Owner 132 + <div className="flex items-center justify-between mb-2"> 133 + <label className="block text-sm font-semibold text-gray-700"> 134 + Repository 97 135 </label> 98 - <input 99 - type="text" 100 - value={owner} 101 - onChange={(e) => setOwner(e.target.value)} 102 - placeholder="e.g., username or organization" 103 - 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" 104 - /> 105 - <p className="mt-1 text-xs text-gray-500"> 106 - Your GitHub username or organization name 107 - </p> 136 + <select 137 + value={sortBy} 138 + onChange={(e) => setSortBy(e.target.value as any)} 139 + className="text-xs px-2 py-1 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-900" 140 + > 141 + <option value="updated">Recently Updated</option> 142 + <option value="created">Recently Created</option> 143 + <option value="name">Name</option> 144 + </select> 108 145 </div> 109 146 110 - <div> 111 - <label className="block text-sm font-semibold text-gray-700 mb-2"> 112 - Repository Name 113 - </label> 114 - <input 115 - type="text" 116 - value={repo} 117 - onChange={(e) => setRepo(e.target.value)} 118 - placeholder="e.g., blog or my-website" 119 - 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" 120 - /> 121 - <p className="mt-1 text-xs text-gray-500"> 122 - The name of your repository 123 - </p> 124 - </div> 147 + {reposLoading ? ( 148 + <div className="w-full px-4 py-12 border border-gray-300 rounded-md bg-gray-50"> 149 + <div className="flex flex-col items-center justify-center"> 150 + <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mb-3"></div> 151 + <p className="text-sm text-gray-600">Loading your repositories...</p> 152 + </div> 153 + </div> 154 + ) : reposError ? ( 155 + <div className="w-full px-4 py-8 border border-red-300 rounded-md bg-red-50"> 156 + <div className="text-center"> 157 + <svg 158 + className="w-12 h-12 text-red-400 mx-auto mb-3" 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="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" 168 + /> 169 + </svg> 170 + <p className="text-sm text-red-800 font-semibold mb-1">Failed to load repositories</p> 171 + <p className="text-xs text-red-600"> 172 + Make sure you've granted MarkEdit access to your repositories. 173 + </p> 174 + </div> 175 + </div> 176 + ) : ( 177 + <> 178 + <select 179 + value={selectedRepoFullName} 180 + onChange={(e) => setSelectedRepoFullName(e.target.value)} 181 + 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" 182 + > 183 + <option value="">Choose a repository...</option> 184 + {repos?.map((repo) => ( 185 + <option key={repo.id} value={repo.full_name}> 186 + {repo.full_name} 187 + {repo.private && ' 🔒'} 188 + </option> 189 + ))} 190 + </select> 125 191 126 - {owner && repo && ( 192 + {repos && repos.length > 0 && ( 193 + <p className="text-xs text-gray-500"> 194 + {repos.length} {repos.length === 1 ? 'repository' : 'repositories'} found 195 + </p> 196 + )} 197 + 198 + {repos && repos.length === 0 && ( 199 + <div className="p-4 bg-amber-50 rounded-lg border border-amber-200"> 200 + <p className="text-sm text-amber-800"> 201 + No repositories found. Make sure you've authorized MarkEdit to access your GitHub repositories. 202 + </p> 203 + </div> 204 + )} 205 + </> 206 + )} 207 + 208 + {selectedRepoFullName && ( 127 209 <div className="p-4 bg-gray-50 rounded-lg border border-gray-200"> 128 210 <div className="text-sm text-gray-700"> 129 - <strong>Full repository path:</strong> 211 + <strong>Selected repository:</strong> 130 212 <div className="mt-1 font-mono text-gray-900"> 131 - github.com/{owner}/{repo} 213 + github.com/{selectedRepoFullName} 132 214 </div> 133 215 </div> 134 216 </div> ··· 145 227 Configure Folder Path 146 228 </h2> 147 229 <p className="text-sm text-gray-600 mb-6"> 148 - Specify which folder contains your blog posts. Leave empty to 149 - use the root directory. 230 + Select which folder contains your blog posts, or leave empty to use the root directory. 150 231 </p> 151 232 </div> 152 233 ··· 155 236 <label className="block text-sm font-semibold text-gray-700 mb-2"> 156 237 Folder Path (optional) 157 238 </label> 158 - <input 159 - type="text" 160 - value={folder} 161 - onChange={(e) => setFolder(e.target.value)} 162 - placeholder="e.g., content/posts or blog" 163 - 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" 164 - /> 165 - <p className="mt-1 text-xs text-gray-500"> 166 - The folder path where your markdown files are located. Leave 167 - empty for root directory. 168 - </p> 239 + 240 + {loadingFolders ? ( 241 + <div className="w-full px-4 py-8 border border-gray-300 rounded-md bg-gray-50"> 242 + <div className="flex flex-col items-center justify-center"> 243 + <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 mb-2"></div> 244 + <p className="text-xs text-gray-600">Loading folders...</p> 245 + </div> 246 + </div> 247 + ) : ( 248 + <> 249 + <select 250 + value={folder} 251 + onChange={(e) => setFolder(e.target.value)} 252 + 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" 253 + > 254 + <option value="">📁 Root directory (all markdown files)</option> 255 + {folders.map((dir) => ( 256 + <option key={dir.path} value={dir.path}> 257 + 📁 {dir.path} 258 + </option> 259 + ))} 260 + </select> 261 + 262 + {folders.length === 0 && !loadingFolders && ( 263 + <p className="mt-2 text-xs text-gray-500"> 264 + No folders found in this repository. You can use the root directory. 265 + </p> 266 + )} 267 + 268 + {folders.length > 0 && ( 269 + <p className="mt-2 text-xs text-gray-500"> 270 + {folders.length} {folders.length === 1 ? 'folder' : 'folders'} found 271 + </p> 272 + )} 273 + </> 274 + )} 169 275 </div> 170 276 171 277 <div className="p-4 bg-amber-50 rounded-lg border border-amber-200"> ··· 184 290 /> 185 291 </svg> 186 292 <div className="text-sm text-amber-800"> 187 - <strong className="block mb-1">Examples:</strong> 188 - <ul className="list-disc list-inside space-y-1"> 189 - <li> 190 - <code className="bg-amber-100 px-1 py-0.5 rounded"> 191 - content/posts 192 - </code>{' '} 193 - - for Hugo/Astro blogs 194 - </li> 195 - <li> 196 - <code className="bg-amber-100 px-1 py-0.5 rounded"> 197 - _posts 198 - </code>{' '} 199 - - for Jekyll blogs 200 - </li> 201 - <li> 202 - <code className="bg-amber-100 px-1 py-0.5 rounded"> 203 - blog 204 - </code>{' '} 205 - - for custom setups 206 - </li> 207 - </ul> 293 + <strong className="block mb-1">Note:</strong> 294 + <p> 295 + MarkEdit will search for markdown files (.md, .mdx) in the selected folder and all its subfolders. 296 + </p> 208 297 </div> 209 298 </div> 210 299 </div> ··· 213 302 <div className="text-sm text-gray-700"> 214 303 <strong>MarkEdit will monitor:</strong> 215 304 <div className="mt-1 font-mono text-gray-900"> 216 - {owner}/{repo}/{folder || '(root)'} 305 + {owner}/{repo}{folder ? `/${folder}` : ' (root)'} 217 306 </div> 218 307 </div> 219 308 </div> ··· 234 323 235 324 <Button 236 325 onClick={handleNext} 237 - disabled={step === 1 && (!owner || !repo)} 326 + disabled={ 327 + (step === 1 && !selectedRepoFullName) || 328 + (step === 1 && reposLoading) 329 + } 238 330 className="bg-gray-900 hover:bg-gray-800 text-white" 239 331 > 240 332 {step === 1 ? 'Next' : 'Complete Setup'}