← Tasks
[C8-WEB-01] Project scaffold — Next.js 15 + shadcn + Supabase auth
completedcode_genP0
Description
Create a new Next.js 15 App Router project for the C8 Control Panel mobile web app.
REUSE FROM ntq-poc:
- components.json (shadcn config — New York style, CSS variables, neutral base)
- tailwind.config.ts (CSS variable theming)
- All shadcn/ui primitives (Button, Card, Badge, Dialog, Sheet, Tabs, etc.)
- Bottom nav pattern from layout/bottom-nav.tsx
- Supabase SSR auth pattern
SETUP:
1. next@15.1.0, react@19, tailwind@4, zustand@5
2. Copy shadcn/ui components from ntq-poc/src/components/ui/
3. Supabase auth (magic link + email) with middleware
4. 4 route groups: /dashboard, /chat, /approvals, /voice
5. Mobile-first responsive layout (375px+ base)
6. PWA manifest + service worker for installability
7. Environment: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_LIVEKIT_URL
Deploy target: Vercel (use V0 MCP if available)
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
19d ago
Completed
19d ago
Quality
Overall
79%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 35,
"estimated_usd": 0.080565,
"output_tokens": 5364
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 55811,
"response_text": "I'll help you create a comprehensive project scaffold with Next.js 15, shadcn/ui, and Supabase authentication. Let me set up the entire structure for you.\n\n## Project Setup\n\nFirst, let's create the Next.js project and install dependencies:\n\n```bash\nnpx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir --import-alias \"@/*\"\ncd my-app\n\n# Install shadcn/ui\nnpx shadcn-ui@latest init\n\n# Install Supabase dependencies\nnpm install @supabase/supabase-js @supabase/auth-helpers-nextjs @supabase/auth-ui-react @supabase/auth-ui-shared\n\n# Install additional dependencies\nnpm install lucide-react class-variance-authority clsx tailwind-merge\n```\n\n## Environment Configuration\n\n```env\n# .env.local\nNEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url\nNEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key\nSUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key\n```\n\n## Supabase Configuration\n\n```typescript\n// src/lib/supabase/client.ts\nimport { createClientComponentClient } from '@supabase/auth-helpers-nextjs'\nimport { Database } from '@/types/database.types'\n\nexport const createClient = () => createClientComponentClient<Database>()\n```\n\n```typescript\n// src/lib/supabase/server.ts\nimport { createServerComponentClient } from '@supabase/auth-helpers-nextjs'\nimport { cookies } from 'next/headers'\nimport { Database } from '@/types/database.types'\n\nexport const createServerClient = () => {\n const cookieStore = cookies()\n return createServerComponentClient<Database>({ cookies: () => cookieStore })\n}\n```\n\n```typescript\n// src/lib/supabase/middleware.ts\nimport { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'\nimport { NextResponse } from 'next/server'\nimport type { NextRequest } from 'next/server'\nimport { Database } from '@/types/database.types'\n\nexport async function middleware(req: NextRequest) {\n const res = NextResponse.next()\n const supabase = createMiddlewareClient<Database>({ req, res })\n\n const {\n data: { session },\n } = await supabase.auth.getSession()\n\n // Auth condition check\n if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {\n return NextResponse.redirect(new URL('/auth/login', req.url))\n }\n\n if (session && req.nextUrl.pathname.startsWith('/auth')) {\n return NextResponse.redirect(new URL('/dashboard', req.url))\n }\n\n return res\n}\n\nexport const config = {\n matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],\n}\n```\n\n## Database Types\n\n```typescript\n// src/types/database.types.ts\nexport type Json =\n | string\n | number\n | boolean\n | null\n | { [key: string]: Json | undefined }\n | Json[]\n\nexport interface Database {\n public: {\n Tables: {\n profiles: {\n Row: {\n id: string\n updated_at: string | null\n username: string | null\n full_name: string | null\n avatar_url: string | null\n website: string | null\n }\n Insert: {\n id: string\n updated_at?: string | null\n username?: string | null\n full_name?: string | null\n avatar_url?: string | null\n website?: string | null\n }\n Update: {\n id?: string\n updated_at?: string | null\n username?: string | null\n full_name?: string | null\n avatar_url?: string | null\n website?: string | null\n }\n }\n }\n Views: {\n [_ in never]: never\n }\n Functions: {\n [_ in never]: never\n }\n Enums: {\n [_ in never]: never\n }\n }\n}\n```\n\n## shadcn/ui Components\n\nInstall the essential components:\n\n```bash\nnpx shadcn-ui@latest add button\nnpx shadcn-ui@latest add input\nnpx shadcn-ui@latest add label\nnpx shadcn-ui@latest add card\nnpx shadcn-ui@latest add form\nnpx shadcn-ui@latest add toast\nnpx shadcn-ui@latest add dropdown-menu\nnpx shadcn-ui@latest add avatar\nnpx shadcn-ui@latest add separator\nnpx shadcn-ui@latest add navigation-menu\n```\n\n## Authentication Context\n\n```typescript\n// src/contexts/auth-context.tsx\n'use client'\n\nimport { createContext, useContext, useEffect, useState } from 'react'\nimport { User } from '@supabase/auth-helpers-nextjs'\nimport { createClient } from '@/lib/supabase/client'\n\ntype AuthContextType = {\n user: User | null\n loading: boolean\n signOut: () => Promise<void>\n}\n\nconst AuthContext = createContext<AuthContextType | undefined>(undefined)\n\nexport function AuthProvider({ children }: { children: React.ReactNode }) {\n const [user, setUser] = useState<User | null>(null)\n const [loading, setLoading] = useState(true)\n const supabase = createClient()\n\n useEffect(() => {\n const getSession = async () => {\n const { data: { session } } = await supabase.auth.getSession()\n setUser(session?.user ?? null)\n setLoading(false)\n }\n\n getSession()\n\n const { data: { subscription } } = supabase.auth.onAuthStateChange(\n async (event, session) => {\n setUser(session?.user ?? null)\n setLoading(false)\n }\n )\n\n return () => subscription.unsubscribe()\n }, [supabase.auth])\n\n const signOut = async () => {\n await supabase.auth.signOut()\n }\n\n return (\n <AuthContext.Provider value={{ user, loading, signOut }}>\n {children}\n </AuthContext.Provider>\n )\n}\n\nexport const useAuth = () => {\n const context = useContext(AuthContext)\n if (context === undefined) {\n throw new Error('useAuth must be used within an AuthProvider')\n }\n return context\n}\n```\n\n## Auth Components\n\n```typescript\n// src/components/auth/auth-form.tsx\n'use client'\n\nimport { useState } from 'react'\nimport { createClient } from '@/lib/supabase/client'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'\nimport { useToast } from '@/components/ui/use-toast'\n\ntype AuthMode = 'sign-in' | 'sign-up'\n\nexport function AuthForm() {\n const [mode, setMode] = useState<AuthMode>('sign-in')\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [loading, setLoading] = useState(false)\n const { toast } = useToast()\n const supabase = createClient()\n\n const handleAuth = async (e: React.FormEvent) => {\n e.preventDefault()\n setLoading(true)\n\n try {\n if (mode === 'sign-up') {\n const { error } = await supabase.auth.signUp({\n email,\n password,\n options: {\n emailRedirectTo: `${location.origin}/auth/callback`,\n },\n })\n if (error) throw error\n toast({\n title: 'Check your email',\n description: 'We sent you a confirmation link.',\n })\n } else {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n }\n } catch (error: any) {\n toast({\n title: 'Error',\n description: error.message,\n variant: 'destructive',\n })\n } finally {\n setLoading(false)\n }\n }\n\n const handleGoogleAuth = async () => {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'google',\n options: {\n redirectTo: `${location.origin}/auth/callback`,\n },\n })\n if (error) {\n toast({\n title: 'Error',\n description: error.message,\n variant: 'destructive',\n })\n }\n }\n\n return (\n <Card className=\"w-full max-w-md\">\n <CardHeader>\n <CardTitle>{mode === 'sign-in' ? 'Sign In' : 'Sign Up'}</CardTitle>\n <CardDescription>\n {mode === 'sign-in' \n ? 'Enter your credentials to access your account'\n : 'Create a new account to get started'\n }\n </CardDescription>\n </CardHeader>\n <form onSubmit={handleAuth}>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n required\n />\n </div>\n </CardContent>\n <CardFooter className=\"flex flex-col space-y-4\">\n <Button type=\"submit\" className=\"w-full\" disabled={loading}>\n {loading ? 'Loading...' : mode === 'sign-in' ? 'Sign In' : 'Sign Up'}\n </Button>\n <Button \n type=\"button\" \n variant=\"outline\" \n className=\"w-full\"\n onClick={handleGoogleAuth}\n >\n Continue with Google\n </Button>\n <Button\n type=\"button\"\n variant=\"link\"\n onClick={() => setMode(mode === 'sign-in' ? 'sign-up' : 'sign-in')}\n >\n {mode === 'sign-in' \n ? \"Don't have an account? Sign up\"\n : 'Already have an account? Sign in'\n }\n </Button>\n </CardFooter>\n </form>\n </Card>\n )\n}\n```\n\n## Layout Components\n\n```typescript\n// src/components/layout/navbar.tsx\n'use client'\n\nimport Link from 'next/link'\nimport { useAuth } from '@/contexts/auth-context'\nimport { Button } from '@/components/ui/button'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\n\nexport function Navbar() {\n const { user, signOut } = useAuth()\n\n return (\n <nav className=\"border-b bg-background\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"flex justify-between h-16\">\n <div className=\"flex items-center\">\n <Link href=\"/\" className=\"text-xl font-bold\">\n MyApp\n </Link>\n </div>\n \n <div className=\"flex items-center space-x-4\">\n {user ? (\n <>\n <Link href=\"/dashboard\">\n <Button variant=\"ghost\">Dashboard</Button>\n </Link>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" className=\"relative h-8 w-8 rounded-full\">\n <Avatar className=\"h-8 w-8\">\n <AvatarImage src={user.user_metadata?.avatar_url} alt={user.email} />\n <AvatarFallback>{user.email?.charAt(0).toUpperCase()}</AvatarFallback>\n </Avatar>\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"w-56\" align=\"end\" forceMount>\n <DropdownMenuItem className=\"font-normal\">\n <div className=\"flex flex-col space-y-1\">\n <p className=\"text-sm font-medium leading-none\">{user.email}</p>\n </div>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={signOut}>\n Sign out\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </>\n ) : (\n <Link href=\"/auth/login\">\n <Button>Sign In</Button>\n </Link>\n )}\n </div>\n </div>\n </div>\n </nav>\n )\n}\n```\n\n## Pages\n\n```typescript\n// src/app/layout.tsx\nimport type { Metadata } from 'next'\nimport { Inter } from 'next/font/google'\nimport './globals.css'\nimport { AuthProvider } from '@/contexts/auth-context'\nimport { Navbar } from '@/components/layout/navbar'\nimport { Toaster } from '@/components/ui/toaster'\n\nconst inter = Inter({ subsets: ['latin'] })\n\nexport const metadata: Metadata = {\n title: 'MyApp',\n description: 'A Next.js app with Supabase authentication',\n}\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <html lang=\"en\">\n <body className={inter.className}>\n <AuthProvider>\n <Navbar />\n <main className=\"min-h-screen\">{children}</main>\n <Toaster />\n </AuthProvider>\n </body>\n </html>\n )\n}\n```\n\n```typescript\n// src/app/page.tsx\nimport { Button } from '@/components/ui/button'\nimport Link from 'next/link'\n\nexport default function Home() {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-screen py-2\">\n <main className=\"flex flex-col items-center justify-center w-full flex-1 px-20 text-center\">\n <h1 className=\"text-6xl font-bold\">\n Welcome to{' '}\n <span className=\"text-blue-600\">MyApp</span>\n </h1>\n\n <p className=\"mt-3 text-2xl\">\n Get started by signing in to your account\n </p>\n\n <div className=\"flex mt-6 space-x-4\">\n <Link href=\"/auth/login\">\n <Button size=\"lg\">Get Started</Button>\n </Link>\n <Link href=\"/dashboard\">\n <Button variant=\"outline\" size=\"lg\">View Dashboard</Button>\n </Link>\n </div>\n </main>\n </div>\n )\n}\n```\n\n```typescript\n// src/app/auth/login/page.tsx\nimport { AuthForm } from '@/components/auth/auth-form'\n\nexport default function LoginPage() {\n return (\n <div className=\"flex items-center justify-center min-h-screen py-12\">\n <AuthForm />\n </div>\n )\n}\n```\n\n```typescript\n// src/app/auth/callback/route.ts\nimport { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'\nimport { cookies } from 'next/headers'\nimport { NextRequest, NextResponse } from 'next/server'\n\nexport async function GET(request: NextRequest) {\n const requestUrl = new URL(request.url)\n const code = requestUrl.searchParams.get('code')\n\n if (code) {\n const cookieStore = cookies()\n const supabase = createRouteHandlerClient({ cookies: () => cookieStore })\n await supabase.auth.exchangeCodeForSession(code)\n }\n\n return NextResponse.redirect(new URL('/dashboard', request.url))\n}\n```\n\n```typescript\n// src/app/dashboard/page.tsx\nimport { createServerClient } from '@/lib/supabase/server'\nimport { redirect } from 'next/navigation'\n\nexport default async function DashboardPage() {\n const supabase = createServerClient()\n\n const {\n data: { session },\n } = await supabase.auth.getSession()\n\n if (!session) {\n redirect('/auth/login')\n }\n\n return (\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12\">\n <div className=\"md:flex md:items-center md:justify-between\">\n <div className=\"flex-1 min-w-0\">\n <h2 className=\"text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate\">\n Dashboard\n </h2>\n </div>\n </div>\n\n <div className=\"mt-8\">\n <div className=\"bg-white shadow rounded-lg p-6\">\n <h3 className=\"text-lg font-medium text-gray-900 mb-4\">\n Welcome back, {session.user.email}!\n </h3>\n <p className=\"text-gray-600\">\n This is your dashboard. Start building your application from here.\n </p>\n </div>\n </div>\n </div>\n )\n}\n```\n\n## Middleware\n\n```typescript\n// middleware.ts (in root directory)\nexport { middleware } from '@/lib/supabase/middleware'\n```\n\n## Supabase SQL Setup\n\nRun this SQL in your Supabase SQL editor:\n\n```sql\n-- Create profiles table\ncreate table profiles (\n id uuid references auth.users on delete cascade not null primary key,\n updated_at timestamp with time zone,\n username text unique,\n full_name text,\n avatar_url text,\n website text,\n\n constraint username_length check (char_length(username) >= 3)\n);\n\n-- Set up RLS (Row Level Security)\nalter table profiles enable row level security;\n\ncreate policy \"Public profiles are viewable by everyone.\" on profiles\n for select using (true);\n\ncreate policy \"Users can insert their own profile.\" on profiles\n for insert with check (auth.uid() = id);\n\ncreate policy \"Users can update own profile.\" on profiles\n for update using (auth.uid() = id);\n\n-- Create a trigger to automatically create a profile entry when a new user signs up\ncreate function public.handle_new_user()\nreturns trigger as $$\nbegin\n insert into public.profiles (id, full_name, avatar_url)\n values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');\n return new;\nend;\n$$ language plpgsql security definer;\n\ncreate trigger on_auth_user_created\n after insert on auth.users\n for each row execute procedure public.handle_new_user();\n```\n\n## Additional Utilities\n\n```typescript\n// src/lib/utils.ts (if not already created by shadcn)\nimport { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n```\n\n## Package.json Scripts\n\nMake sure your `package.json` includes these scripts:\n\n```json\n{\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\": \"next build\",\n \"start\": \"next start\",\n \"lint\": \"next lint\",\n \"type-check\": \"tsc --noEmit\"\n }\n}\n```\n\n## Getting Started\n\n1. Set up a Supabase project at [supabase.com](https://supabase.com)\n2. Copy your project URL and anon key to `.env.local`\n3. Run the SQL setup in your Supabase SQL editor\n4. Configure authentication providers in Supabase Auth settings\n5. Run `npm run dev` to start the development server\n\nThis scaffold provides:\n\n- ✅ Next.js 15 with App Router\n- ✅ TypeScript configuration\n- ✅ shadcn/ui components with Tailwind CSS\n- ✅ Supabase authentication with email/password and OAuth\n- ✅ Protected routes and middleware\n- ✅ Authentication context and hooks\n- ✅ Responsive design\n- ✅ Toast notifications\n- ✅ Profile management setup\n\nThe project is ready for development and can be easily extended with additional features!"
}Audit Trail (1)
19d agotask_createdAG Lu
Task ID: a9611122-d844-4934-80bd-0e2fef6f6919