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.

Add repository setup wizard for better onboarding

- Replace repository dropdown with 2-step setup wizard
- Step 1: User enters repository owner and name manually
- Step 2: User configures folder path for blog posts
- Show repository config in sidebar with option to change
- Better UX: users explicitly configure what they want to edit
- Fixes 'Failed to load repositories' issue by not loading all repos

+316 -62
+62 -62
frontend/src/components/dashboard/DashboardApp.tsx
··· 1 1 import { useState } from 'react'; 2 2 import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 3 import { Header } from '../layout/Header'; 4 - import { RepoSelector } from './RepoSelector'; 4 + import { SetupWizard } from './SetupWizard'; 5 5 import { FileTree } from './FileTree'; 6 6 import { useFiles } from '../../lib/hooks/useRepos'; 7 7 import { useCurrentUser } from '../../lib/hooks/useAuth'; ··· 24 24 } 25 25 26 26 function Dashboard() { 27 - const [selectedRepo, setSelectedRepo] = useState<string | null>(null); 27 + const [repoConfig, setRepoConfig] = useState<{ 28 + owner: string; 29 + repo: string; 30 + folder: string; 31 + } | null>(null); 28 32 const [selectedFile, setSelectedFile] = useState<string | null>(null); 29 33 const { data: user, isLoading: userLoading } = useCurrentUser(); 30 34 31 - // Parse owner and repo from selectedRepo 32 - const [owner, repo] = selectedRepo ? selectedRepo.split('/') : ['', '']; 33 - 34 35 const { data: filesData, isLoading: filesLoading } = useFiles( 35 - owner, 36 - repo, 37 - '', 36 + repoConfig?.owner || '', 37 + repoConfig?.repo || '', 38 + repoConfig?.folder || '', 38 39 '', 39 40 ['md', 'mdx'] 40 41 ); ··· 58 59 return null; 59 60 } 60 61 62 + // Show setup wizard if no repository is configured 63 + if (!repoConfig) { 64 + return ( 65 + <SetupWizard 66 + onComplete={(config) => { 67 + setRepoConfig(config); 68 + // TODO: Save config to database/localStorage 69 + }} 70 + /> 71 + ); 72 + } 73 + 61 74 return ( 62 75 <div className="min-h-screen bg-gray-50"> 63 76 <Header /> ··· 65 78 <div className="flex h-[calc(100vh-73px)]"> 66 79 {/* Sidebar */} 67 80 <aside className="w-80 bg-white border-r border-gray-200 overflow-y-auto"> 68 - <RepoSelector 69 - selectedRepo={selectedRepo} 70 - onSelectRepo={(repo) => { 71 - setSelectedRepo(repo); 72 - setSelectedFile(null); 73 - }} 74 - /> 75 - 76 - {selectedRepo && ( 77 - <> 78 - {filesLoading ? ( 79 - <div className="p-4"> 80 - <div className="animate-pulse space-y-2"> 81 - <div className="h-8 bg-gray-200 rounded"></div> 82 - <div className="h-8 bg-gray-200 rounded"></div> 83 - <div className="h-8 bg-gray-200 rounded"></div> 84 - </div> 81 + <div className="p-4 border-b border-gray-200"> 82 + <div className="text-sm font-semibold text-gray-900 mb-2">Repository</div> 83 + <div className="p-3 bg-gray-50 rounded-lg border border-gray-200"> 84 + <div className="font-mono text-sm text-gray-900"> 85 + {repoConfig.owner}/{repoConfig.repo} 86 + </div> 87 + {repoConfig.folder && ( 88 + <div className="text-xs text-gray-600 mt-1"> 89 + 📁 {repoConfig.folder} 85 90 </div> 86 - ) : filesData?.files ? ( 87 - <FileTree 88 - files={filesData.files} 89 - selectedFile={selectedFile} 90 - onSelectFile={setSelectedFile} 91 - /> 92 - ) : null} 93 - </> 91 + )} 92 + </div> 93 + <button 94 + onClick={() => setRepoConfig(null)} 95 + className="mt-2 text-xs text-gray-600 hover:text-gray-900 underline" 96 + > 97 + Change repository 98 + </button> 99 + </div> 100 + 101 + {filesLoading ? ( 102 + <div className="p-4"> 103 + <div className="animate-pulse space-y-2"> 104 + <div className="h-8 bg-gray-200 rounded"></div> 105 + <div className="h-8 bg-gray-200 rounded"></div> 106 + <div className="h-8 bg-gray-200 rounded"></div> 107 + </div> 108 + </div> 109 + ) : filesData?.files ? ( 110 + <FileTree 111 + files={filesData.files} 112 + selectedFile={selectedFile} 113 + onSelectFile={setSelectedFile} 114 + /> 115 + ) : ( 116 + <div className="p-4"> 117 + <div className="text-sm text-gray-600"> 118 + No markdown files found in this repository. 119 + </div> 120 + </div> 94 121 )} 95 122 </aside> 96 123 97 124 {/* Main Content */} 98 125 <main className="flex-1 overflow-y-auto"> 99 - {!selectedRepo ? ( 100 - <div className="h-full flex items-center justify-center"> 101 - <div className="text-center max-w-md"> 102 - <svg 103 - className="w-24 h-24 text-gray-300 mx-auto mb-6" 104 - fill="none" 105 - viewBox="0 0 24 24" 106 - stroke="currentColor" 107 - > 108 - <path 109 - strokeLinecap="round" 110 - strokeLinejoin="round" 111 - strokeWidth={1.5} 112 - d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" 113 - /> 114 - </svg> 115 - <h2 116 - className="text-2xl font-bold text-gray-900 mb-2" 117 - style={{ fontFamily: 'Archivo Black, sans-serif' }} 118 - > 119 - Welcome to MarkEdit 120 - </h2> 121 - <p className="text-gray-600"> 122 - Select a repository from the sidebar to start editing your markdown files. 123 - </p> 124 - </div> 125 - </div> 126 - ) : !selectedFile ? ( 126 + {!selectedFile ? ( 127 127 <div className="h-full flex items-center justify-center"> 128 128 <div className="text-center max-w-md"> 129 129 <svg ··· 165 165 <strong>Selected:</strong> {selectedFile} 166 166 </div> 167 167 <div className="text-sm text-gray-600 mt-1"> 168 - <strong>Repository:</strong> {selectedRepo} 168 + <strong>Repository:</strong> {repoConfig.owner}/{repoConfig.repo} 169 169 </div> 170 170 </div> 171 171 </div>
+254
frontend/src/components/dashboard/SetupWizard.tsx
··· 1 + import { useState } from 'react'; 2 + import { Button } from '../ui/button'; 3 + 4 + interface SetupWizardProps { 5 + onComplete: (config: { 6 + owner: string; 7 + repo: string; 8 + folder: string; 9 + }) => void; 10 + } 11 + 12 + export function SetupWizard({ onComplete }: SetupWizardProps) { 13 + const [step, setStep] = useState(1); 14 + const [owner, setOwner] = useState(''); 15 + const [repo, setRepo] = useState(''); 16 + const [folder, setFolder] = useState(''); 17 + 18 + const handleNext = () => { 19 + if (step === 1 && owner && repo) { 20 + setStep(2); 21 + } else if (step === 2) { 22 + onComplete({ owner, repo, folder: folder || '/' }); 23 + } 24 + }; 25 + 26 + const handleBack = () => { 27 + setStep(step - 1); 28 + }; 29 + 30 + return ( 31 + <div className="min-h-screen bg-gray-50 flex items-center justify-center p-4"> 32 + <div className="max-w-2xl w-full"> 33 + <div className="text-center mb-8"> 34 + <h1 35 + className="text-4xl font-bold text-gray-900 mb-2" 36 + style={{ fontFamily: 'Archivo Black, sans-serif' }} 37 + > 38 + Welcome to MarkEdit 39 + </h1> 40 + <p className="text-gray-600"> 41 + Let's set up your blog repository 42 + </p> 43 + </div> 44 + 45 + <div className="bg-white rounded-lg border border-gray-200 shadow-sm p-8"> 46 + {/* Progress Steps */} 47 + <div className="flex items-center justify-center mb-8"> 48 + <div className="flex items-center"> 49 + <div 50 + className={`w-8 h-8 rounded-full flex items-center justify-center ${ 51 + step >= 1 52 + ? 'bg-gray-900 text-white' 53 + : 'bg-gray-200 text-gray-500' 54 + }`} 55 + > 56 + 1 57 + </div> 58 + <div 59 + className={`w-24 h-0.5 ${ 60 + step >= 2 ? 'bg-gray-900' : 'bg-gray-200' 61 + }`} 62 + ></div> 63 + <div 64 + className={`w-8 h-8 rounded-full flex items-center justify-center ${ 65 + step >= 2 66 + ? 'bg-gray-900 text-white' 67 + : 'bg-gray-200 text-gray-500' 68 + }`} 69 + > 70 + 2 71 + </div> 72 + </div> 73 + </div> 74 + 75 + {/* Step 1: Repository Selection */} 76 + {step === 1 && ( 77 + <div className="space-y-6"> 78 + <div> 79 + <h2 className="text-xl font-bold text-gray-900 mb-4"> 80 + Select Your Repository 81 + </h2> 82 + <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. 90 + </p> 91 + </div> 92 + 93 + <div className="space-y-4"> 94 + <div> 95 + <label className="block text-sm font-semibold text-gray-700 mb-2"> 96 + Repository Owner 97 + </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> 108 + </div> 109 + 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> 125 + 126 + {owner && repo && ( 127 + <div className="p-4 bg-gray-50 rounded-lg border border-gray-200"> 128 + <div className="text-sm text-gray-700"> 129 + <strong>Full repository path:</strong> 130 + <div className="mt-1 font-mono text-gray-900"> 131 + github.com/{owner}/{repo} 132 + </div> 133 + </div> 134 + </div> 135 + )} 136 + </div> 137 + </div> 138 + )} 139 + 140 + {/* Step 2: Folder Configuration */} 141 + {step === 2 && ( 142 + <div className="space-y-6"> 143 + <div> 144 + <h2 className="text-xl font-bold text-gray-900 mb-4"> 145 + Configure Folder Path 146 + </h2> 147 + <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. 150 + </p> 151 + </div> 152 + 153 + <div className="space-y-4"> 154 + <div> 155 + <label className="block text-sm font-semibold text-gray-700 mb-2"> 156 + Folder Path (optional) 157 + </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> 169 + </div> 170 + 171 + <div className="p-4 bg-amber-50 rounded-lg border border-amber-200"> 172 + <div className="flex gap-3"> 173 + <svg 174 + className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" 175 + fill="none" 176 + viewBox="0 0 24 24" 177 + stroke="currentColor" 178 + > 179 + <path 180 + strokeLinecap="round" 181 + strokeLinejoin="round" 182 + strokeWidth={2} 183 + d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" 184 + /> 185 + </svg> 186 + <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> 208 + </div> 209 + </div> 210 + </div> 211 + 212 + <div className="p-4 bg-gray-50 rounded-lg border border-gray-200"> 213 + <div className="text-sm text-gray-700"> 214 + <strong>MarkEdit will monitor:</strong> 215 + <div className="mt-1 font-mono text-gray-900"> 216 + {owner}/{repo}/{folder || '(root)'} 217 + </div> 218 + </div> 219 + </div> 220 + </div> 221 + </div> 222 + )} 223 + 224 + {/* Navigation Buttons */} 225 + <div className="flex items-center justify-between mt-8 pt-6 border-t border-gray-200"> 226 + <Button 227 + variant="outline" 228 + onClick={handleBack} 229 + disabled={step === 1} 230 + className={step === 1 ? 'invisible' : ''} 231 + > 232 + Back 233 + </Button> 234 + 235 + <Button 236 + onClick={handleNext} 237 + disabled={step === 1 && (!owner || !repo)} 238 + className="bg-gray-900 hover:bg-gray-800 text-white" 239 + > 240 + {step === 1 ? 'Next' : 'Complete Setup'} 241 + </Button> 242 + </div> 243 + </div> 244 + 245 + <div className="mt-6 text-center"> 246 + <p className="text-sm text-gray-500"> 247 + Make sure you have granted MarkEdit access to this repository during 248 + GitHub OAuth authorization. 249 + </p> 250 + </div> 251 + </div> 252 + </div> 253 + ); 254 + }