Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	Reupload OmniDev clean version
Browse files- components/new/scaffold.tsx +89 -25
 - lib/hero-presets.ts +15 -0
 - lib/new-stacks.ts +30 -0
 
    	
        components/new/scaffold.tsx
    CHANGED
    
    | 
         @@ -5,19 +5,54 @@ import { useAi } from "@/hooks/useAi"; 
     | 
|
| 5 | 
         
             
            import { useRouter } from "next/navigation";
         
     | 
| 6 | 
         
             
            import { Button } from "@/components/ui/button";
         
     | 
| 7 | 
         
             
            import { toast } from "sonner";
         
     | 
| 
         | 
|
| 
         | 
|
| 8 | 
         | 
| 9 | 
         
            -
             
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 10 | 
         | 
| 11 | 
         
             
            Requirements:
         
     | 
| 12 | 
         
            -
            - Frontend under /frontend using React + Vite ( 
     | 
| 13 | 
         
            -
            - Backend under /backend using Express ( 
     | 
| 14 | 
         
            -
            -  
     | 
| 
         | 
|
| 15 | 
         
             
            - Provide package.json in both /frontend and /backend with scripts to start dev/prod.
         
     | 
| 16 | 
         
            -
            - Provide /frontend/index.html and  
     | 
| 17 | 
         
            -
            - Provide /backend/server 
     | 
| 18 | 
         
             
            - Use ports 5173 for frontend and 3000 for backend.
         
     | 
| 19 | 
         
             
            - Keep everything simple and runnable.
         
     | 
| 20 | 
         
            -
            - Return JSON  
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 21 | 
         | 
| 22 | 
         
             
            export default function ScaffoldNew() {
         
     | 
| 23 | 
         
             
              const { user, openLoginWindow } = useUser();
         
     | 
| 
         @@ -26,6 +61,10 @@ export default function ScaffoldNew() { 
     | 
|
| 26 | 
         
             
              const [loading, setLoading] = useState(false);
         
     | 
| 27 | 
         
             
              const [error, setError] = useState<string | null>(null);
         
     | 
| 28 | 
         
             
              const [logs, setLogs] = useState<string[]>([]);
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 29 | 
         | 
| 30 | 
         
             
              async function runScaffold() {
         
     | 
| 31 | 
         
             
                if (!user) {
         
     | 
| 
         @@ -37,14 +76,15 @@ export default function ScaffoldNew() { 
     | 
|
| 37 | 
         
             
                setLogs(["Starting scaffold via Augment..."]);
         
     | 
| 38 | 
         
             
                try {
         
     | 
| 39 | 
         
             
                  // 1) Call augment to generate full-stack files
         
     | 
| 
         | 
|
| 40 | 
         
             
                  const aug = await fetch('/api/augment', {
         
     | 
| 41 | 
         
             
                    method: 'POST',
         
     | 
| 42 | 
         
             
                    headers: { 'Content-Type': 'application/json' },
         
     | 
| 43 | 
         
             
                    body: JSON.stringify({
         
     | 
| 44 | 
         
             
                      context: '/ (empty project)',
         
     | 
| 45 | 
         
            -
                      instruction 
     | 
| 46 | 
         
            -
                      language: 'javascript',
         
     | 
| 47 | 
         
            -
                      framework:  
     | 
| 48 | 
         
             
                      response_type: 'file_updates',
         
     | 
| 49 | 
         
             
                      model,
         
     | 
| 50 | 
         
             
                      provider,
         
     | 
| 
         @@ -56,11 +96,10 @@ export default function ScaffoldNew() { 
     | 
|
| 56 | 
         
             
                  setLogs(prev => [...prev, `Augment produced ${aug.files.length} files`]);
         
     | 
| 57 | 
         | 
| 58 | 
         
             
                  // 2) Create space with initial files
         
     | 
| 59 | 
         
            -
                  const title = 'OmniDev Full-Stack Project';
         
     | 
| 60 | 
         
             
                  const created = await fetch('/api/me/projects', {
         
     | 
| 61 | 
         
             
                    method: 'POST',
         
     | 
| 62 | 
         
             
                    headers: { 'Content-Type': 'application/json' },
         
     | 
| 63 | 
         
            -
                    body: JSON.stringify({ title, initialFiles: aug.files.map((f: any) => ({ path: f.path, content: f.content })) })
         
     | 
| 64 | 
         
             
                  }).then(r => r.json());
         
     | 
| 65 | 
         
             
                  if (!created?.space?.project?.space_id) {
         
     | 
| 66 | 
         
             
                    throw new Error(created?.error || 'Failed to create project');
         
     | 
| 
         @@ -75,24 +114,49 @@ export default function ScaffoldNew() { 
     | 
|
| 75 | 
         
             
                }
         
     | 
| 76 | 
         
             
              }
         
     | 
| 77 | 
         | 
| 78 | 
         
            -
               
     | 
| 79 | 
         
            -
                // Auto-run once when visiting /new
         
     | 
| 80 | 
         
            -
                runScaffold();
         
     | 
| 81 | 
         
            -
                // eslint-disable-next-line react-hooks/exhaustive-deps
         
     | 
| 82 | 
         
            -
              }, []);
         
     | 
| 83 | 
         | 
| 84 | 
         
             
              return (
         
     | 
| 85 | 
         
            -
                <section className="max-w- 
     | 
| 86 | 
         
            -
                  <h1 className="text-2xl font-semibold mb- 
     | 
| 87 | 
         
            -
                  <p className="text-sm text-neutral-400 mb-4"> 
     | 
| 88 | 
         
            -
                   
     | 
| 89 | 
         
            -
             
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 90 | 
         
             
                    {logs.map((l, i) => (<li key={i}>• {l}</li>))}
         
     | 
| 91 | 
         
             
                  </ul>
         
     | 
| 92 | 
         
            -
                  <div className="mt- 
     | 
| 93 | 
         
            -
                    <Button size="sm" onClick={runScaffold} disabled={loading}>{loading ? 'Working...' : ' 
     | 
| 94 | 
         
             
                  </div>
         
     | 
| 95 | 
         
             
                </section>
         
     | 
| 96 | 
         
             
              );
         
     | 
| 97 | 
         
             
            }
         
     | 
| 98 | 
         
            -
             
     | 
| 
         | 
|
| 5 | 
         
             
            import { useRouter } from "next/navigation";
         
     | 
| 6 | 
         
             
            import { Button } from "@/components/ui/button";
         
     | 
| 7 | 
         
             
            import { toast } from "sonner";
         
     | 
| 8 | 
         
            +
            import { NEW_STACKS, NewStackId } from "@/lib/new-stacks";
         
     | 
| 9 | 
         
            +
            import { HERO_STYLES } from "@/lib/hero-presets";
         
     | 
| 10 | 
         | 
| 11 | 
         
            +
            function buildInstruction(stack: NewStackId, lang: "js" | "ts", hero: string, title?: string) {
         
     | 
| 12 | 
         
            +
              const jsOrTs = lang === 'ts' ? 'TypeScript' : 'JavaScript';
         
     | 
| 13 | 
         
            +
              const reactEntry = lang === 'ts' ? "/frontend/src/main.tsx and /frontend/src/App.tsx" : "/frontend/src/main.jsx and /frontend/src/App.jsx";
         
     | 
| 14 | 
         
            +
             
     | 
| 15 | 
         
            +
              switch (stack) {
         
     | 
| 16 | 
         
            +
                case 'express-react':
         
     | 
| 17 | 
         
            +
                  return `Initialize a complete full-stack web project with the following structure and runnable code.
         
     | 
| 18 | 
         
            +
             
     | 
| 19 | 
         
            +
            Title: ${title || 'OmniDev Full-Stack App'}
         
     | 
| 20 | 
         | 
| 21 | 
         
             
            Requirements:
         
     | 
| 22 | 
         
            +
            - Frontend under /frontend using React + Vite (${jsOrTs}), TailwindCSS preconfigured.
         
     | 
| 23 | 
         
            +
            - Backend under /backend using Express (${jsOrTs}, ESM), with basic routes (GET /, GET /health) and CORS enabled.
         
     | 
| 24 | 
         
            +
            - Use a Hero section with style: ${hero}. Keep it performant and accessible.
         
     | 
| 25 | 
         
            +
            - Add a minimal README.md at root with start instructions.
         
     | 
| 26 | 
         
             
            - Provide package.json in both /frontend and /backend with scripts to start dev/prod.
         
     | 
| 27 | 
         
            +
            - Provide /frontend/index.html and ${reactEntry}.
         
     | 
| 28 | 
         
            +
            - Provide /backend/server.${lang === 'ts' ? 'ts' : 'js'}.
         
     | 
| 29 | 
         
             
            - Use ports 5173 for frontend and 3000 for backend.
         
     | 
| 30 | 
         
             
            - Keep everything simple and runnable.
         
     | 
| 31 | 
         
            +
            - Return STRICT JSON ONLY as file updates (no markdown), paths rooted from repo (e.g., /frontend/..., /backend/...).`;
         
     | 
| 32 | 
         
            +
                case 'nextjs':
         
     | 
| 33 | 
         
            +
                  return `Scaffold a full-stack Next.js 15 App Router project.
         
     | 
| 34 | 
         
            +
             
     | 
| 35 | 
         
            +
            Title: ${title || 'OmniDev Next App'}
         
     | 
| 36 | 
         
            +
             
     | 
| 37 | 
         
            +
            Requirements:
         
     | 
| 38 | 
         
            +
            - Next.js (${jsOrTs}), App Router, TailwindCSS.
         
     | 
| 39 | 
         
            +
            - Implement a landing page with a Hero section style: ${hero}.
         
     | 
| 40 | 
         
            +
            - Add /api/health route that returns { ok: true }.
         
     | 
| 41 | 
         
            +
            - Provide package.json with dev/build scripts.
         
     | 
| 42 | 
         
            +
            - Keep it simple and runnable with \'next dev\'.
         
     | 
| 43 | 
         
            +
            - Return STRICT JSON ONLY as file updates (no markdown), paths rooted from repo (e.g., /app/page.${lang === 'ts' ? 'tsx' : 'jsx'}, /app/api/health/route.${lang === 'ts' ? 'ts' : 'js'}).`;
         
     | 
| 44 | 
         
            +
                case 'nestjs-react':
         
     | 
| 45 | 
         
            +
                  return `Initialize a NestJS backend and React (Vite) frontend.
         
     | 
| 46 | 
         
            +
             
     | 
| 47 | 
         
            +
            Title: ${title || 'OmniDev Nest + React'}
         
     | 
| 48 | 
         
            +
             
     | 
| 49 | 
         
            +
            Requirements:
         
     | 
| 50 | 
         
            +
            - Backend under /backend using NestJS (${jsOrTs}). Generate AppModule, AppController with GET / and GET /health. Enable CORS.
         
     | 
| 51 | 
         
            +
            - Frontend under /frontend using React + Vite (${jsOrTs}), TailwindCSS with a modern Hero section style: ${hero}.
         
     | 
| 52 | 
         
            +
            - Provide package.json in both apps with start/build scripts.
         
     | 
| 53 | 
         
            +
            - Return STRICT JSON ONLY as file updates (no markdown), paths rooted from repo.`;
         
     | 
| 54 | 
         
            +
              }
         
     | 
| 55 | 
         
            +
            }
         
     | 
| 56 | 
         | 
| 57 | 
         
             
            export default function ScaffoldNew() {
         
     | 
| 58 | 
         
             
              const { user, openLoginWindow } = useUser();
         
     | 
| 
         | 
|
| 61 | 
         
             
              const [loading, setLoading] = useState(false);
         
     | 
| 62 | 
         
             
              const [error, setError] = useState<string | null>(null);
         
     | 
| 63 | 
         
             
              const [logs, setLogs] = useState<string[]>([]);
         
     | 
| 64 | 
         
            +
              const [stack, setStack] = useState<NewStackId>("express-react");
         
     | 
| 65 | 
         
            +
              const [lang, setLang] = useState<"js" | "ts">("js");
         
     | 
| 66 | 
         
            +
              const [hero, setHero] = useState<string>(HERO_STYLES[0].id);
         
     | 
| 67 | 
         
            +
              const [title, setTitle] = useState<string>("");
         
     | 
| 68 | 
         | 
| 69 | 
         
             
              async function runScaffold() {
         
     | 
| 70 | 
         
             
                if (!user) {
         
     | 
| 
         | 
|
| 76 | 
         
             
                setLogs(["Starting scaffold via Augment..."]);
         
     | 
| 77 | 
         
             
                try {
         
     | 
| 78 | 
         
             
                  // 1) Call augment to generate full-stack files
         
     | 
| 79 | 
         
            +
                  const instruction = buildInstruction(stack, lang, hero, title);
         
     | 
| 80 | 
         
             
                  const aug = await fetch('/api/augment', {
         
     | 
| 81 | 
         
             
                    method: 'POST',
         
     | 
| 82 | 
         
             
                    headers: { 'Content-Type': 'application/json' },
         
     | 
| 83 | 
         
             
                    body: JSON.stringify({
         
     | 
| 84 | 
         
             
                      context: '/ (empty project)',
         
     | 
| 85 | 
         
            +
                      instruction,
         
     | 
| 86 | 
         
            +
                      language: lang === 'ts' ? 'typescript' : 'javascript',
         
     | 
| 87 | 
         
            +
                      framework: stack,
         
     | 
| 88 | 
         
             
                      response_type: 'file_updates',
         
     | 
| 89 | 
         
             
                      model,
         
     | 
| 90 | 
         
             
                      provider,
         
     | 
| 
         | 
|
| 96 | 
         
             
                  setLogs(prev => [...prev, `Augment produced ${aug.files.length} files`]);
         
     | 
| 97 | 
         | 
| 98 | 
         
             
                  // 2) Create space with initial files
         
     | 
| 
         | 
|
| 99 | 
         
             
                  const created = await fetch('/api/me/projects', {
         
     | 
| 100 | 
         
             
                    method: 'POST',
         
     | 
| 101 | 
         
             
                    headers: { 'Content-Type': 'application/json' },
         
     | 
| 102 | 
         
            +
                    body: JSON.stringify({ title: title || 'OmniDev Project', initialFiles: aug.files.map((f: any) => ({ path: f.path, content: f.content })) })
         
     | 
| 103 | 
         
             
                  }).then(r => r.json());
         
     | 
| 104 | 
         
             
                  if (!created?.space?.project?.space_id) {
         
     | 
| 105 | 
         
             
                    throw new Error(created?.error || 'Failed to create project');
         
     | 
| 
         | 
|
| 114 | 
         
             
                }
         
     | 
| 115 | 
         
             
              }
         
     | 
| 116 | 
         | 
| 117 | 
         
            +
              // Removed auto-run; wait for user selection
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 118 | 
         | 
| 119 | 
         
             
              return (
         
     | 
| 120 | 
         
            +
                <section className="max-w-3xl mx-auto p-6 text-neutral-200">
         
     | 
| 121 | 
         
            +
                  <h1 className="text-2xl font-semibold mb-1">Create New Project</h1>
         
     | 
| 122 | 
         
            +
                  <p className="text-sm text-neutral-400 mb-4">Choose your stack and hero style, then OmniDev will scaffold a complete project.</p>
         
     | 
| 123 | 
         
            +
                  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
         
     | 
| 124 | 
         
            +
                    <div>
         
     | 
| 125 | 
         
            +
                      <label className="text-sm block mb-1">Project Title</label>
         
     | 
| 126 | 
         
            +
                      <input className="w-full bg-neutral-800 rounded p-2 text-sm" placeholder="OmniDev Project" value={title} onChange={(e) => setTitle(e.target.value)} />
         
     | 
| 127 | 
         
            +
                    </div>
         
     | 
| 128 | 
         
            +
                    <div>
         
     | 
| 129 | 
         
            +
                      <label className="text-sm block mb-1">Language</label>
         
     | 
| 130 | 
         
            +
                      <select className="w-full bg-neutral-800 rounded p-2 text-sm" value={lang} onChange={(e) => setLang(e.target.value as any)}>
         
     | 
| 131 | 
         
            +
                        <option value="js">JavaScript</option>
         
     | 
| 132 | 
         
            +
                        <option value="ts">TypeScript</option>
         
     | 
| 133 | 
         
            +
                      </select>
         
     | 
| 134 | 
         
            +
                    </div>
         
     | 
| 135 | 
         
            +
                    <div>
         
     | 
| 136 | 
         
            +
                      <label className="text-sm block mb-1">Stack</label>
         
     | 
| 137 | 
         
            +
                      <select className="w-full bg-neutral-800 rounded p-2 text-sm" value={stack} onChange={(e) => setStack(e.target.value as NewStackId)}>
         
     | 
| 138 | 
         
            +
                        {NEW_STACKS.map(s => (
         
     | 
| 139 | 
         
            +
                          <option key={s.id} value={s.id}>{s.label}</option>
         
     | 
| 140 | 
         
            +
                        ))}
         
     | 
| 141 | 
         
            +
                      </select>
         
     | 
| 142 | 
         
            +
                      <p className="text-xs text-neutral-500 mt-1">{NEW_STACKS.find(s => s.id === stack)?.description}</p>
         
     | 
| 143 | 
         
            +
                    </div>
         
     | 
| 144 | 
         
            +
                    <div>
         
     | 
| 145 | 
         
            +
                      <label className="text-sm block mb-1">Hero Style</label>
         
     | 
| 146 | 
         
            +
                      <select className="w-full bg-neutral-800 rounded p-2 text-sm" value={hero} onChange={(e) => setHero(e.target.value)}>
         
     | 
| 147 | 
         
            +
                        {HERO_STYLES.map(h => (
         
     | 
| 148 | 
         
            +
                          <option key={h.id} value={h.id}>{h.label}</option>
         
     | 
| 149 | 
         
            +
                        ))}
         
     | 
| 150 | 
         
            +
                      </select>
         
     | 
| 151 | 
         
            +
                    </div>
         
     | 
| 152 | 
         
            +
                  </div>
         
     | 
| 153 | 
         
            +
                  {error && <p className="text-red-400 text-sm mt-3">{error}</p>}
         
     | 
| 154 | 
         
            +
                  <ul className="text-sm text-neutral-400 space-y-1 mt-3">
         
     | 
| 155 | 
         
             
                    {logs.map((l, i) => (<li key={i}>• {l}</li>))}
         
     | 
| 156 | 
         
             
                  </ul>
         
     | 
| 157 | 
         
            +
                  <div className="mt-5">
         
     | 
| 158 | 
         
            +
                    <Button size="sm" onClick={runScaffold} disabled={loading}>{loading ? 'Working...' : 'Create Project'}</Button>
         
     | 
| 159 | 
         
             
                  </div>
         
     | 
| 160 | 
         
             
                </section>
         
     | 
| 161 | 
         
             
              );
         
     | 
| 162 | 
         
             
            }
         
     | 
| 
         | 
    	
        lib/hero-presets.ts
    ADDED
    
    | 
         @@ -0,0 +1,15 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            export const HERO_STYLES = [
         
     | 
| 2 | 
         
            +
              { id: "animated-gradient", label: "Animated Gradient" },
         
     | 
| 3 | 
         
            +
              { id: "particles", label: "Particles" },
         
     | 
| 4 | 
         
            +
              { id: "waves", label: "Waves" },
         
     | 
| 5 | 
         
            +
              { id: "noise", label: "Noise Overlay" },
         
     | 
| 6 | 
         
            +
              { id: "parallax", label: "Parallax Layers" },
         
     | 
| 7 | 
         
            +
              { id: "vanta-globe", label: "Vanta Globe" },
         
     | 
| 8 | 
         
            +
              { id: "morphing-blobs", label: "Morphing Blobs" },
         
     | 
| 9 | 
         
            +
              { id: "rays", label: "Rays / Aurora" },
         
     | 
| 10 | 
         
            +
              { id: "grid", label: "Animated Grid" },
         
     | 
| 11 | 
         
            +
              { id: "shapes", label: "Floating Shapes" },
         
     | 
| 12 | 
         
            +
              { id: "lines", label: "Moving Lines" },
         
     | 
| 13 | 
         
            +
              { id: "orbits", label: "Orbits" },
         
     | 
| 14 | 
         
            +
            ];
         
     | 
| 15 | 
         
            +
             
     | 
    	
        lib/new-stacks.ts
    ADDED
    
    | 
         @@ -0,0 +1,30 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            export type NewStackId = "express-react" | "nextjs" | "nestjs-react";
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            export interface NewStack {
         
     | 
| 4 | 
         
            +
              id: NewStackId;
         
     | 
| 5 | 
         
            +
              label: string;
         
     | 
| 6 | 
         
            +
              description: string;
         
     | 
| 7 | 
         
            +
              frameworkHint: string; // sent to /api/augment as framework
         
     | 
| 8 | 
         
            +
            }
         
     | 
| 9 | 
         
            +
             
     | 
| 10 | 
         
            +
            export const NEW_STACKS: NewStack[] = [
         
     | 
| 11 | 
         
            +
              {
         
     | 
| 12 | 
         
            +
                id: "express-react",
         
     | 
| 13 | 
         
            +
                label: "Express + React (Vite)",
         
     | 
| 14 | 
         
            +
                description: "React (Vite + Tailwind) for frontend and Express (ESM) for backend.",
         
     | 
| 15 | 
         
            +
                frameworkHint: "express-react",
         
     | 
| 16 | 
         
            +
              },
         
     | 
| 17 | 
         
            +
              {
         
     | 
| 18 | 
         
            +
                id: "nextjs",
         
     | 
| 19 | 
         
            +
                label: "Next.js (App Router)",
         
     | 
| 20 | 
         
            +
                description: "Full-stack Next.js 15 (App Router) with Tailwind and an /api route.",
         
     | 
| 21 | 
         
            +
                frameworkHint: "nextjs",
         
     | 
| 22 | 
         
            +
              },
         
     | 
| 23 | 
         
            +
              {
         
     | 
| 24 | 
         
            +
                id: "nestjs-react",
         
     | 
| 25 | 
         
            +
                label: "NestJS + React",
         
     | 
| 26 | 
         
            +
                description: "NestJS backend + React (Vite + Tailwind) frontend.",
         
     | 
| 27 | 
         
            +
                frameworkHint: "nestjs-react",
         
     | 
| 28 | 
         
            +
              },
         
     | 
| 29 | 
         
            +
            ];
         
     | 
| 30 | 
         
            +
             
     |