template
This commit is contained in:
25
frontend/.env.local.example
Normal file
25
frontend/.env.local.example
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# ENVIRONMENT VARIABLES
|
||||||
|
# =============================================================================
|
||||||
|
# Copy this file to .env.local and fill in your values
|
||||||
|
# NEVER commit .env.local to version control!
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# SUPABASE CONFIGURATION
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Get these from: https://app.supabase.com/project/_/settings/api
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# BACKEND API URL
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# In development, this is proxied through Next.js (see next.config.js)
|
||||||
|
# In production, point to your deployed FastAPI backend
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# AI PROVIDER KEYS (Optional - can also be in backend only)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Only add here if you need client-side AI calls (not recommended for security)
|
||||||
|
# NEXT_PUBLIC_OPENAI_API_KEY=sk-xxx
|
||||||
17
frontend/components.json
Normal file
17
frontend/components.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
frontend/next-env.d.ts
vendored
Normal file
5
frontend/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||||
33
frontend/next.config.js
Normal file
33
frontend/next.config.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* NEXT.JS CONFIGURATION
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This configures the Next.js build and runtime behavior.
|
||||||
|
*
|
||||||
|
* Key features:
|
||||||
|
* - API rewrites to proxy requests to FastAPI backend (avoids CORS issues)
|
||||||
|
* - Image optimization settings
|
||||||
|
* - Environment variables handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
// Rewrites let us proxy API calls to the FastAPI backend
|
||||||
|
// This avoids CORS issues during development
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/api/backend/:path*",
|
||||||
|
destination: "http://localhost:8000/api/:path*", // FastAPI backend
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
// Image optimization - add domains you'll load images from
|
||||||
|
images: {
|
||||||
|
domains: ["your-supabase-project.supabase.co"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
2090
frontend/package-lock.json
generated
Normal file
2090
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
frontend/package.json
Normal file
34
frontend/package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "hack-nation-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^14.2.28",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"@supabase/supabase-js": "^2.47.0",
|
||||||
|
"@supabase/ssr": "^0.5.2",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.0",
|
||||||
|
"lucide-react": "^0.460.0",
|
||||||
|
"tailwind-merge": "^2.5.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"@types/react": "^18.2.48",
|
||||||
|
"@types/react-dom": "^18.2.18",
|
||||||
|
"autoprefixer": "^10.4.17",
|
||||||
|
"postcss": "^8.4.33",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
60
frontend/src/app/auth/callback/route.ts
Normal file
60
frontend/src/app/auth/callback/route.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* AUTH CALLBACK ROUTE
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This is a Route Handler (API route in App Router) that handles OAuth
|
||||||
|
* callback redirects from Supabase.
|
||||||
|
*
|
||||||
|
* When a user signs in with Google/GitHub:
|
||||||
|
* 1. They're redirected to the OAuth provider
|
||||||
|
* 2. Provider redirects back here with a code
|
||||||
|
* 3. We exchange the code for a session
|
||||||
|
* 4. Redirect the user to the dashboard
|
||||||
|
*
|
||||||
|
* Route: /auth/callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createServerClient } from "@supabase/ssr";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
import type { CookieOptions } from "@supabase/ssr";
|
||||||
|
|
||||||
|
interface CookieToSet {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
options: CookieOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const requestUrl = new URL(request.url);
|
||||||
|
const code = requestUrl.searchParams.get("code");
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
// Create Supabase client with cookies for server-side
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const supabase = createServerClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
{
|
||||||
|
cookies: {
|
||||||
|
getAll() {
|
||||||
|
return cookieStore.getAll();
|
||||||
|
},
|
||||||
|
setAll(cookiesToSet: CookieToSet[]) {
|
||||||
|
cookiesToSet.forEach(({ name, value, options }) => {
|
||||||
|
cookieStore.set(name, value, options);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exchange the code for a session
|
||||||
|
await supabase.auth.exchangeCodeForSession(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to dashboard after successful authentication
|
||||||
|
return NextResponse.redirect(new URL("/dashboard", request.url));
|
||||||
|
}
|
||||||
127
frontend/src/app/dashboard/page.tsx
Normal file
127
frontend/src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* DASHBOARD PAGE (Protected Route Example)
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This page demonstrates:
|
||||||
|
* - A protected route (requires authentication)
|
||||||
|
* - Fetching user data from Supabase
|
||||||
|
* - Client-side data fetching
|
||||||
|
*
|
||||||
|
* Route: /dashboard
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client"; // This makes it a Client Component (needed for hooks)
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useSupabase } from "@/lib/supabase/provider";
|
||||||
|
import type { User } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
// Get Supabase client and auth state from our custom hook
|
||||||
|
const { supabase, user, loading } = useSupabase();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Local state for any dashboard-specific data
|
||||||
|
const [projects, setProjects] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// Redirect to login if not authenticated
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && !user) {
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
}, [user, loading, router]);
|
||||||
|
|
||||||
|
// Fetch user's projects from Supabase (example)
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchProjects() {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
// Example: Fetch projects from Supabase
|
||||||
|
// Uncomment when you have a 'projects' table:
|
||||||
|
// const { data, error } = await supabase
|
||||||
|
// .from("projects")
|
||||||
|
// .select("*")
|
||||||
|
// .eq("user_id", user.id);
|
||||||
|
//
|
||||||
|
// if (data) setProjects(data);
|
||||||
|
|
||||||
|
// Mock data for demo
|
||||||
|
setProjects([
|
||||||
|
{ id: 1, name: "AI Chatbot", status: "In Progress" },
|
||||||
|
{ id: 2, name: "Image Generator", status: "Completed" },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchProjects();
|
||||||
|
}, [user, supabase]);
|
||||||
|
|
||||||
|
// Show loading state while checking auth
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't render anything if not authenticated (will redirect)
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-8">Dashboard</h1>
|
||||||
|
|
||||||
|
{/* User Info Card */}
|
||||||
|
<Card className="mb-8">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Welcome back!</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Logged in as {user.email}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
User ID: {user.id}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Projects Grid */}
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Your Projects</h2>
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{projects.map((project) => (
|
||||||
|
<Card key={project.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">{project.name}</CardTitle>
|
||||||
|
<CardDescription>{project.status}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
Open Project
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* New Project Card */}
|
||||||
|
<Card className="border-dashed">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">+ New Project</CardTitle>
|
||||||
|
<CardDescription>Start a new AI project</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
frontend/src/app/demo/page.tsx
Normal file
50
frontend/src/app/demo/page.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* DEMO PAGE
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This page showcases various features you can use:
|
||||||
|
* - AI text generation
|
||||||
|
* - AI image generation (placeholder)
|
||||||
|
* - Database CRUD operations
|
||||||
|
*
|
||||||
|
* Route: /demo
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { AIDemo } from "@/components/ai-demo";
|
||||||
|
import { DatabaseDemo } from "@/components/database-demo";
|
||||||
|
|
||||||
|
export default function DemoPage() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-2">Feature Demos</h1>
|
||||||
|
<p className="text-muted-foreground mb-8">
|
||||||
|
Explore the different features available in this template
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Tabs for different demo sections */}
|
||||||
|
<Tabs defaultValue="ai" className="w-full">
|
||||||
|
<TabsList className="mb-8">
|
||||||
|
<TabsTrigger value="ai">AI Generation</TabsTrigger>
|
||||||
|
<TabsTrigger value="database">Database (Supabase)</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* AI Demo Tab */}
|
||||||
|
<TabsContent value="ai">
|
||||||
|
<div className="space-y-8">
|
||||||
|
<section>
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Text Generation</h2>
|
||||||
|
<AIDemo />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Database Demo Tab */}
|
||||||
|
<TabsContent value="database">
|
||||||
|
<DatabaseDemo />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
frontend/src/app/globals.css
Normal file
84
frontend/src/app/globals.css
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* GLOBAL STYLES & SHADCN/UI THEME
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This file contains:
|
||||||
|
* 1. Tailwind CSS directives
|
||||||
|
* 2. CSS variables for Shadcn/ui theming
|
||||||
|
* 3. Global base styles
|
||||||
|
*
|
||||||
|
* Shadcn/ui uses CSS variables so you can easily customize the entire theme
|
||||||
|
* by changing values here. Both light and dark themes are defined.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
/*
|
||||||
|
* Light theme colors
|
||||||
|
* These are HSL values without the hsl() wrapper
|
||||||
|
* Format: hue saturation% lightness%
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Dark theme colors
|
||||||
|
* Automatically applied when .dark class is on html/body
|
||||||
|
*/
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
/* Apply theme colors to all elements */
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
frontend/src/app/layout.tsx
Normal file
63
frontend/src/app/layout.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* ROOT LAYOUT
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This is the root layout that wraps ALL pages in your Next.js app.
|
||||||
|
*
|
||||||
|
* Key responsibilities:
|
||||||
|
* - HTML structure and metadata
|
||||||
|
* - Global providers (auth, theme, etc.)
|
||||||
|
* - Persistent UI elements (navigation, footer)
|
||||||
|
*
|
||||||
|
* In Next.js App Router:
|
||||||
|
* - layout.tsx files wrap their child routes
|
||||||
|
* - They persist across page navigations (don't re-render)
|
||||||
|
* - Great for shared UI and providers
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
// @ts-ignore - CSS imports don't need type declarations
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
// Import providers
|
||||||
|
import { Providers } from "@/components/providers";
|
||||||
|
import { Navigation } from "@/components/navigation";
|
||||||
|
|
||||||
|
// Load Inter font with Latin subset
|
||||||
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
// Metadata for SEO - customize for your hackathon project!
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Hack Nation 2026 - AI-Powered App",
|
||||||
|
description: "Built with Next.js, FastAPI, and AI",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<body className={inter.className}>
|
||||||
|
{/*
|
||||||
|
Providers wrap the entire app with context providers:
|
||||||
|
- Supabase auth context
|
||||||
|
- Theme context (light/dark mode)
|
||||||
|
- Any other global state
|
||||||
|
*/}
|
||||||
|
<Providers>
|
||||||
|
{/* Navigation is persistent across all pages */}
|
||||||
|
<Navigation />
|
||||||
|
|
||||||
|
{/* Main content area */}
|
||||||
|
<main className="min-h-screen">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</Providers>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
194
frontend/src/app/login/page.tsx
Normal file
194
frontend/src/app/login/page.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* LOGIN PAGE
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This page demonstrates:
|
||||||
|
* - Supabase authentication (email/password & OAuth)
|
||||||
|
* - Form handling in React
|
||||||
|
* - Redirecting after successful login
|
||||||
|
*
|
||||||
|
* Supabase Auth Features Used:
|
||||||
|
* - signInWithPassword: Email/password login
|
||||||
|
* - signInWithOAuth: Social login (Google, GitHub, etc.)
|
||||||
|
* - signUp: Create new account
|
||||||
|
*
|
||||||
|
* Route: /login
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { useSupabase } from "@/lib/supabase/provider";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const { supabase } = useSupabase();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isSignUp, setIsSignUp] = useState(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle email/password authentication
|
||||||
|
*/
|
||||||
|
async function handleEmailAuth(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isSignUp) {
|
||||||
|
// Create new account
|
||||||
|
const { error } = await supabase.auth.signUp({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
options: {
|
||||||
|
// Redirect URL after email confirmation
|
||||||
|
emailRedirectTo: `${window.location.origin}/auth/callback`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Show success message (Supabase sends confirmation email)
|
||||||
|
setError("Check your email to confirm your account!");
|
||||||
|
} else {
|
||||||
|
// Sign in existing user
|
||||||
|
const { error } = await supabase.auth.signInWithPassword({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Redirect to dashboard on success
|
||||||
|
router.push("/dashboard");
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle OAuth login (Google, GitHub, etc.)
|
||||||
|
*/
|
||||||
|
async function handleOAuthLogin(provider: "google" | "github") {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { error } = await supabase.auth.signInWithOAuth({
|
||||||
|
provider,
|
||||||
|
options: {
|
||||||
|
redirectTo: `${window.location.origin}/auth/callback`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen px-4">
|
||||||
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<CardTitle>{isSignUp ? "Create Account" : "Welcome Back"}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{isSignUp
|
||||||
|
? "Sign up to start building with AI"
|
||||||
|
: "Sign in to your account"}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
{/* OAuth Buttons */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleOAuthLogin("github")}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleOAuthLogin("google")}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Google
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="relative mb-6">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<span className="w-full border-t" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-background px-2 text-muted-foreground">
|
||||||
|
Or continue with email
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email/Password Form */}
|
||||||
|
<form onSubmit={handleEmailAuth} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-destructive">{error}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" disabled={loading}>
|
||||||
|
{loading ? "Loading..." : isSignUp ? "Sign Up" : "Sign In"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardFooter className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() => setIsSignUp(!isSignUp)}
|
||||||
|
>
|
||||||
|
{isSignUp
|
||||||
|
? "Already have an account? Sign in"
|
||||||
|
: "Don't have an account? Sign up"}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
frontend/src/app/page.tsx
Normal file
105
frontend/src/app/page.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* HOME PAGE
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This is the main landing page of your app (route: /)
|
||||||
|
*
|
||||||
|
* In Next.js App Router:
|
||||||
|
* - page.tsx files define the UI for a route
|
||||||
|
* - This file is at /app/page.tsx, so it renders at /
|
||||||
|
* - /app/about/page.tsx would render at /about
|
||||||
|
*
|
||||||
|
* This page demonstrates:
|
||||||
|
* - Using Shadcn/ui components
|
||||||
|
* - Calling the AI endpoint
|
||||||
|
* - Basic layout structure
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { AIDemo } from "@/components/ai-demo";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="text-center py-16">
|
||||||
|
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
|
||||||
|
🚀 Hack Nation 2026
|
||||||
|
</h1>
|
||||||
|
<p className="mt-6 text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
A full-stack template with Next.js, FastAPI, Supabase, and AI integration.
|
||||||
|
Everything you need to build your hackathon project!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA Buttons using Shadcn Button component */}
|
||||||
|
<div className="mt-8 flex gap-4 justify-center">
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link href="/dashboard">Get Started</Link>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="lg" asChild>
|
||||||
|
<Link href="/demo">View Demo</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Feature Cards */}
|
||||||
|
<section className="grid md:grid-cols-3 gap-6 py-12">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>⚡ Next.js 14</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
React framework with App Router
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Server components, streaming, and the latest React features.
|
||||||
|
File-based routing makes navigation simple.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>🎨 Shadcn/ui</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Beautiful, accessible components
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Copy-paste components built on Radix UI and Tailwind.
|
||||||
|
Fully customizable and accessible by default.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>🤖 AI Integration</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
FastAPI + OpenAI/Anthropic
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Python backend for AI processing. Connect to any LLM provider.
|
||||||
|
Streaming responses supported.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* AI Demo Section */}
|
||||||
|
<section className="py-12">
|
||||||
|
<h2 className="text-2xl font-bold text-center mb-8">
|
||||||
|
Try the AI Demo
|
||||||
|
</h2>
|
||||||
|
<AIDemo />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
frontend/src/components/ai-demo.tsx
Normal file
107
frontend/src/components/ai-demo.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* AI DEMO COMPONENT
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This component demonstrates how to:
|
||||||
|
* - Call your FastAPI AI endpoint
|
||||||
|
* - Handle loading states
|
||||||
|
* - Display AI-generated responses
|
||||||
|
*
|
||||||
|
* The actual AI processing happens in the FastAPI backend.
|
||||||
|
* This keeps your API keys secure on the server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { generateText, type AIGenerateResponse } from "@/lib/api";
|
||||||
|
|
||||||
|
export function AIDemo() {
|
||||||
|
// State for the prompt input
|
||||||
|
const [prompt, setPrompt] = useState("");
|
||||||
|
|
||||||
|
// State for the AI response
|
||||||
|
const [response, setResponse] = useState<AIGenerateResponse | null>(null);
|
||||||
|
|
||||||
|
// Loading and error states
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle form submission - calls the AI endpoint
|
||||||
|
*/
|
||||||
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!prompt.trim()) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResponse(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call the FastAPI backend
|
||||||
|
const result = await generateText(prompt);
|
||||||
|
setResponse(result);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || "Failed to generate response");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="max-w-2xl mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>🤖 AI Text Generation</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter a prompt and the AI will generate a response
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{/* Prompt Input */}
|
||||||
|
<Textarea
|
||||||
|
placeholder="Enter your prompt here... e.g., 'Write a haiku about coding'"
|
||||||
|
value={prompt}
|
||||||
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
className="resize-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<Button type="submit" disabled={loading || !prompt.trim()}>
|
||||||
|
{loading ? "Generating..." : "Generate"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Error Display */}
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 bg-destructive/10 text-destructive rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Response Display */}
|
||||||
|
{response && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-semibold">Response:</h3>
|
||||||
|
<div className="p-4 bg-muted rounded-lg whitespace-pre-wrap">
|
||||||
|
{response.text}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Model: {response.model}
|
||||||
|
{response.usage && (
|
||||||
|
<> • Tokens: {response.usage.prompt_tokens + response.usage.completion_tokens}</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
260
frontend/src/components/database-demo.tsx
Normal file
260
frontend/src/components/database-demo.tsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* DATABASE DEMO COMPONENT
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This component demonstrates Supabase database operations:
|
||||||
|
* - Real-time subscriptions
|
||||||
|
* - CRUD operations (Create, Read, Update, Delete)
|
||||||
|
* - Row Level Security (RLS) integration
|
||||||
|
*
|
||||||
|
* Supabase uses Postgres under the hood, giving you a full SQL database
|
||||||
|
* with real-time capabilities and automatic REST APIs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { useSupabase } from "@/lib/supabase/provider";
|
||||||
|
|
||||||
|
// Define the shape of a todo item (match your Supabase table)
|
||||||
|
interface Todo {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
completed: boolean;
|
||||||
|
created_at: string;
|
||||||
|
user_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DatabaseDemo() {
|
||||||
|
const { supabase, user } = useSupabase();
|
||||||
|
|
||||||
|
// State for todos
|
||||||
|
const [todos, setTodos] = useState<Todo[]>([]);
|
||||||
|
const [newTodo, setNewTodo] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch todos from Supabase
|
||||||
|
* Uses the auto-generated REST API
|
||||||
|
*/
|
||||||
|
async function fetchTodos() {
|
||||||
|
if (!user) {
|
||||||
|
setTodos([]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("todos")
|
||||||
|
.select("*")
|
||||||
|
.order("created_at", { ascending: false });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error fetching todos:", error);
|
||||||
|
} else {
|
||||||
|
setTodos(data || []);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new todo
|
||||||
|
*/
|
||||||
|
async function addTodo(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newTodo.trim() || !user) return;
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("todos")
|
||||||
|
.insert([{ text: newTodo, user_id: user.id }])
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error adding todo:", error);
|
||||||
|
} else if (data) {
|
||||||
|
setTodos([data, ...todos]);
|
||||||
|
setNewTodo("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle todo completion
|
||||||
|
*/
|
||||||
|
async function toggleTodo(id: string, completed: boolean) {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("todos")
|
||||||
|
.update({ completed: !completed })
|
||||||
|
.eq("id", id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error updating todo:", error);
|
||||||
|
} else {
|
||||||
|
setTodos(todos.map(t =>
|
||||||
|
t.id === id ? { ...t, completed: !completed } : t
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a todo
|
||||||
|
*/
|
||||||
|
async function deleteTodo(id: string) {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("todos")
|
||||||
|
.delete()
|
||||||
|
.eq("id", id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error deleting todo:", error);
|
||||||
|
} else {
|
||||||
|
setTodos(todos.filter(t => t.id !== id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch todos on mount and when user changes
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTodos();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
// Set up real-time subscription
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
// Subscribe to changes on the todos table
|
||||||
|
const channel = supabase
|
||||||
|
.channel("todos-changes")
|
||||||
|
.on(
|
||||||
|
"postgres_changes",
|
||||||
|
{
|
||||||
|
event: "*", // Listen to all events (INSERT, UPDATE, DELETE)
|
||||||
|
schema: "public",
|
||||||
|
table: "todos",
|
||||||
|
filter: `user_id=eq.${user.id}`,
|
||||||
|
},
|
||||||
|
(payload) => {
|
||||||
|
console.log("Real-time update:", payload);
|
||||||
|
// Refetch to stay in sync (or update state directly)
|
||||||
|
fetchTodos();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
// Cleanup subscription on unmount
|
||||||
|
return () => {
|
||||||
|
supabase.removeChannel(channel);
|
||||||
|
};
|
||||||
|
}, [user, supabase]);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>📊 Database Demo</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Please log in to see the database demo
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>📊 Database Demo (Supabase)</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Real-time CRUD operations with Supabase
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Add Todo Form */}
|
||||||
|
<form onSubmit={addTodo} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Add a new todo..."
|
||||||
|
value={newTodo}
|
||||||
|
onChange={(e) => setNewTodo(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button type="submit">Add</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Setup Instructions */}
|
||||||
|
<div className="p-4 bg-muted rounded-lg text-sm">
|
||||||
|
<p className="font-semibold mb-2">⚠️ Setup Required:</p>
|
||||||
|
<p>Create a `todos` table in Supabase with this SQL:</p>
|
||||||
|
<pre className="mt-2 p-2 bg-background rounded text-xs overflow-x-auto">
|
||||||
|
{`CREATE TABLE todos (
|
||||||
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
completed BOOLEAN DEFAULT FALSE,
|
||||||
|
user_id UUID REFERENCES auth.users(id),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Enable Row Level Security
|
||||||
|
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Users can only see their own todos
|
||||||
|
CREATE POLICY "Users can view own todos"
|
||||||
|
ON todos FOR SELECT
|
||||||
|
USING (auth.uid() = user_id);
|
||||||
|
|
||||||
|
-- Users can insert their own todos
|
||||||
|
CREATE POLICY "Users can insert own todos"
|
||||||
|
ON todos FOR INSERT
|
||||||
|
WITH CHECK (auth.uid() = user_id);
|
||||||
|
|
||||||
|
-- Users can update their own todos
|
||||||
|
CREATE POLICY "Users can update own todos"
|
||||||
|
ON todos FOR UPDATE
|
||||||
|
USING (auth.uid() = user_id);
|
||||||
|
|
||||||
|
-- Users can delete their own todos
|
||||||
|
CREATE POLICY "Users can delete own todos"
|
||||||
|
ON todos FOR DELETE
|
||||||
|
USING (auth.uid() = user_id);`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Todo List */}
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-muted-foreground">Loading...</p>
|
||||||
|
) : todos.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground">No todos yet. Add one above!</p>
|
||||||
|
) : (
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{todos.map((todo) => (
|
||||||
|
<li
|
||||||
|
key={todo.id}
|
||||||
|
className="flex items-center gap-2 p-2 bg-muted rounded"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={todo.completed}
|
||||||
|
onChange={() => toggleTodo(todo.id, todo.completed)}
|
||||||
|
className="w-4 h-4"
|
||||||
|
aria-label={`Mark "${todo.text}" as ${todo.completed ? 'incomplete' : 'complete'}`}
|
||||||
|
/>
|
||||||
|
<span className={todo.completed ? "line-through text-muted-foreground" : ""}>
|
||||||
|
{todo.text}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="ml-auto"
|
||||||
|
onClick={() => deleteTodo(todo.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
frontend/src/components/navigation.tsx
Normal file
83
frontend/src/components/navigation.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* NAVIGATION COMPONENT
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This is the main navigation bar that appears on all pages.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Logo/brand
|
||||||
|
* - Navigation links
|
||||||
|
* - Auth state (login/logout button)
|
||||||
|
* - Mobile responsive (add hamburger menu if needed)
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useSupabase } from "@/lib/supabase/provider";
|
||||||
|
|
||||||
|
export function Navigation() {
|
||||||
|
const { user, supabase, loading } = useSupabase();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle user logout
|
||||||
|
*/
|
||||||
|
async function handleLogout() {
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="border-b">
|
||||||
|
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
|
||||||
|
{/* Logo / Brand */}
|
||||||
|
<Link href="/" className="font-bold text-xl">
|
||||||
|
🚀 Hack Nation
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Navigation Links */}
|
||||||
|
<div className="hidden md:flex items-center gap-6">
|
||||||
|
<Link
|
||||||
|
href="/demo"
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Demo
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/dashboard"
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Auth Section */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{loading ? (
|
||||||
|
// Show loading state while checking auth
|
||||||
|
<div className="h-9 w-20 bg-muted animate-pulse rounded" />
|
||||||
|
) : user ? (
|
||||||
|
// User is logged in
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-sm text-muted-foreground hidden sm:block">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
|
<Button variant="outline" onClick={handleLogout}>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// User is not logged in
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/login">Login</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
frontend/src/components/providers.tsx
Normal file
33
frontend/src/components/providers.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* GLOBAL PROVIDERS
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This component wraps all the context providers your app needs.
|
||||||
|
*
|
||||||
|
* Having a single Providers component keeps the root layout clean
|
||||||
|
* and makes it easy to add/remove providers.
|
||||||
|
*
|
||||||
|
* Current providers:
|
||||||
|
* - SupabaseProvider: Authentication and database
|
||||||
|
*
|
||||||
|
* Add more as needed:
|
||||||
|
* - ThemeProvider: Dark/light mode
|
||||||
|
* - QueryClientProvider: React Query for data fetching
|
||||||
|
* - etc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SupabaseProvider } from "@/lib/supabase/provider";
|
||||||
|
|
||||||
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<SupabaseProvider>
|
||||||
|
{/* Add more providers here as needed */}
|
||||||
|
{/* <ThemeProvider> */}
|
||||||
|
{children}
|
||||||
|
{/* </ThemeProvider> */}
|
||||||
|
</SupabaseProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
frontend/src/components/ui/button.tsx
Normal file
84
frontend/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* SHADCN/UI BUTTON COMPONENT
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This is a pre-built Shadcn/ui component.
|
||||||
|
*
|
||||||
|
* Shadcn/ui philosophy:
|
||||||
|
* - Components are copied into your project (not imported from node_modules)
|
||||||
|
* - This means you OWN the code and can customize freely
|
||||||
|
* - Built on Radix UI primitives for accessibility
|
||||||
|
* - Styled with Tailwind CSS
|
||||||
|
*
|
||||||
|
* To add more components:
|
||||||
|
* npx shadcn-ui@latest add [component-name]
|
||||||
|
*
|
||||||
|
* Available components: button, card, input, dialog, dropdown-menu, etc.
|
||||||
|
* See: https://ui.shadcn.com/docs/components
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button variants using class-variance-authority (CVA)
|
||||||
|
* CVA makes it easy to define variant styles with Tailwind
|
||||||
|
*/
|
||||||
|
const buttonVariants = cva(
|
||||||
|
// Base styles applied to all buttons
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
// Visual variants
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
// Size variants
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
/**
|
||||||
|
* If true, the button will render as its child element (for use with Link, etc.)
|
||||||
|
* Example: <Button asChild><Link href="/page">Click</Link></Button>
|
||||||
|
*/
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
// Slot allows rendering as a different element while preserving styles
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
100
frontend/src/components/ui/card.tsx
Normal file
100
frontend/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* SHADCN/UI CARD COMPONENT
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* A versatile card component for displaying content in a contained area.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* <Card>
|
||||||
|
* <CardHeader>
|
||||||
|
* <CardTitle>Title</CardTitle>
|
||||||
|
* <CardDescription>Description</CardDescription>
|
||||||
|
* </CardHeader>
|
||||||
|
* <CardContent>
|
||||||
|
* Content goes here
|
||||||
|
* </CardContent>
|
||||||
|
* <CardFooter>
|
||||||
|
* Footer content
|
||||||
|
* </CardFooter>
|
||||||
|
* </Card>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Card.displayName = "Card";
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
));
|
||||||
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||||
33
frontend/src/components/ui/input.tsx
Normal file
33
frontend/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* SHADCN/UI INPUT COMPONENT
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* A styled input component that matches the design system.
|
||||||
|
* Supports all native input attributes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
export { Input };
|
||||||
73
frontend/src/components/ui/tabs.tsx
Normal file
73
frontend/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* SHADCN/UI TABS COMPONENT
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* A tabs component built on Radix UI Tabs primitive.
|
||||||
|
* Provides keyboard navigation and accessibility out of the box.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* <Tabs defaultValue="tab1">
|
||||||
|
* <TabsList>
|
||||||
|
* <TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
||||||
|
* <TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
||||||
|
* </TabsList>
|
||||||
|
* <TabsContent value="tab1">Content 1</TabsContent>
|
||||||
|
* <TabsContent value="tab2">Content 2</TabsContent>
|
||||||
|
* </Tabs>
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
31
frontend/src/components/ui/textarea.tsx
Normal file
31
frontend/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* SHADCN/UI TEXTAREA COMPONENT
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* A styled textarea component for multi-line text input.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface TextareaProps
|
||||||
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Textarea.displayName = "Textarea";
|
||||||
|
|
||||||
|
export { Textarea };
|
||||||
147
frontend/src/lib/api.ts
Normal file
147
frontend/src/lib/api.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* API CLIENT
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This module provides typed functions for calling your FastAPI backend.
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* - Next.js proxies /api/backend/* to FastAPI (see next.config.js)
|
||||||
|
* - This avoids CORS issues during development
|
||||||
|
* - In production, you might call FastAPI directly
|
||||||
|
*
|
||||||
|
* Benefits:
|
||||||
|
* - Centralized API calls
|
||||||
|
* - Type safety with TypeScript
|
||||||
|
* - Easy to add auth headers, error handling, etc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Base URL for API calls (proxied through Next.js)
|
||||||
|
const API_BASE = "/api/backend";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic fetch wrapper with error handling
|
||||||
|
*/
|
||||||
|
async function fetchAPI<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const url = `${API_BASE}${endpoint}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(error.detail || `API Error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AI ENDPOINTS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response type from AI generate endpoint
|
||||||
|
*/
|
||||||
|
export interface AIGenerateResponse {
|
||||||
|
text: string;
|
||||||
|
model: string;
|
||||||
|
usage?: {
|
||||||
|
prompt_tokens: number;
|
||||||
|
completion_tokens: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate text using AI
|
||||||
|
*/
|
||||||
|
export async function generateText(prompt: string): Promise<AIGenerateResponse> {
|
||||||
|
return fetchAPI<AIGenerateResponse>("/ai/generate", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ prompt }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chat with AI (for conversation-style interactions)
|
||||||
|
*/
|
||||||
|
export interface ChatMessage {
|
||||||
|
role: "user" | "assistant" | "system";
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatResponse {
|
||||||
|
message: ChatMessage;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function chat(messages: ChatMessage[]): Promise<ChatResponse> {
|
||||||
|
return fetchAPI<ChatResponse>("/ai/chat", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ messages }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// USER DATA ENDPOINTS (Examples)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
user_id: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProjects(): Promise<Project[]> {
|
||||||
|
return fetchAPI<Project[]>("/projects");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createProject(data: Omit<Project, "id" | "user_id" | "created_at">): Promise<Project> {
|
||||||
|
return fetchAPI<Project>("/projects", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// STREAMING AI (For real-time responses)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream AI responses for real-time text generation
|
||||||
|
* Uses Server-Sent Events (SSE) for streaming
|
||||||
|
*/
|
||||||
|
export async function* streamGenerate(prompt: string): AsyncGenerator<string> {
|
||||||
|
const response = await fetch(`${API_BASE}/ai/stream`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ prompt }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API Error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
if (!reader) throw new Error("No response body");
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value);
|
||||||
|
yield chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
frontend/src/lib/supabase/client.ts
Normal file
40
frontend/src/lib/supabase/client.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* SUPABASE CLIENT (Browser/Client-Side)
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This creates a Supabase client for use in Client Components (browser).
|
||||||
|
*
|
||||||
|
* Supabase provides:
|
||||||
|
* - Authentication (email, OAuth, magic links)
|
||||||
|
* - Database (Postgres with real-time subscriptions)
|
||||||
|
* - Storage (file uploads)
|
||||||
|
* - Edge Functions (serverless functions)
|
||||||
|
*
|
||||||
|
* Use this client in:
|
||||||
|
* - Client Components ("use client")
|
||||||
|
* - Event handlers
|
||||||
|
* - useEffect hooks
|
||||||
|
*
|
||||||
|
* For Server Components, use the server client instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createBrowserClient } from "@supabase/ssr";
|
||||||
|
|
||||||
|
// These are public keys - safe to expose in browser
|
||||||
|
// Provide fallback empty strings to prevent build errors when env vars are not set
|
||||||
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
||||||
|
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Supabase client for client-side operations
|
||||||
|
*/
|
||||||
|
export function createClient() {
|
||||||
|
if (!supabaseUrl || !supabaseAnonKey) {
|
||||||
|
console.warn(
|
||||||
|
"⚠️ Supabase environment variables not set. " +
|
||||||
|
"Copy .env.local.example to .env.local and add your Supabase credentials."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return createBrowserClient(supabaseUrl, supabaseAnonKey);
|
||||||
|
}
|
||||||
87
frontend/src/lib/supabase/provider.tsx
Normal file
87
frontend/src/lib/supabase/provider.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* SUPABASE CONTEXT PROVIDER
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This React Context provides Supabase client and auth state throughout the app.
|
||||||
|
*
|
||||||
|
* Why use a context?
|
||||||
|
* - Single source of truth for auth state
|
||||||
|
* - Avoids creating multiple Supabase client instances
|
||||||
|
* - Makes it easy to access auth from any component
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* const { supabase, user, loading } = useSupabase();
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
import { createClient } from "./client";
|
||||||
|
import type { SupabaseClient, User, Session, AuthChangeEvent } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
// Define the shape of our context
|
||||||
|
interface SupabaseContext {
|
||||||
|
supabase: SupabaseClient;
|
||||||
|
user: User | null;
|
||||||
|
session: Session | null;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the context with undefined default (we'll check for this)
|
||||||
|
const Context = createContext<SupabaseContext | undefined>(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supabase Provider Component
|
||||||
|
* Wrap your app with this to access Supabase anywhere
|
||||||
|
*/
|
||||||
|
export function SupabaseProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
// Create Supabase client once
|
||||||
|
const [supabase] = useState(() => createClient());
|
||||||
|
|
||||||
|
// Track auth state
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Get initial session
|
||||||
|
supabase.auth.getSession().then(({ data: { session: initialSession } }: { data: { session: Session | null } }) => {
|
||||||
|
setSession(initialSession);
|
||||||
|
setUser(initialSession?.user ?? null);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for auth changes (login, logout, token refresh)
|
||||||
|
const {
|
||||||
|
data: { subscription },
|
||||||
|
} = supabase.auth.onAuthStateChange((_event: AuthChangeEvent, newSession: Session | null) => {
|
||||||
|
setSession(newSession);
|
||||||
|
setUser(newSession?.user ?? null);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup subscription on unmount
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [supabase]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={{ supabase, user, session, loading }}>
|
||||||
|
{children}
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to access Supabase context
|
||||||
|
* Throws error if used outside of SupabaseProvider
|
||||||
|
*/
|
||||||
|
export function useSupabase() {
|
||||||
|
const context = useContext(Context);
|
||||||
|
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useSupabase must be used within a SupabaseProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
23
frontend/src/lib/utils.ts
Normal file
23
frontend/src/lib/utils.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* CN UTILITY FUNCTION
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This is a utility function used by Shadcn/ui components for merging
|
||||||
|
* class names with Tailwind CSS.
|
||||||
|
*
|
||||||
|
* How it works:
|
||||||
|
* - clsx: Conditionally joins class names together
|
||||||
|
* - twMerge: Intelligently merges Tailwind classes (handles conflicts)
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* cn("px-4 py-2", isActive && "bg-blue-500", "px-8")
|
||||||
|
* Result: "py-2 px-8 bg-blue-500" (px-8 overrides px-4)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
73
frontend/tailwind.config.ts
Normal file
73
frontend/tailwind.config.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* TAILWIND CSS CONFIGURATION
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* This config is set up for Shadcn/ui components.
|
||||||
|
* Shadcn/ui uses CSS variables for theming, making it easy to customize colors.
|
||||||
|
*
|
||||||
|
* To add new Shadcn components, run:
|
||||||
|
* npx shadcn-ui@latest add <component-name>
|
||||||
|
*
|
||||||
|
* Example: npx shadcn-ui@latest add button
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
// Enable dark mode via class (allows toggle)
|
||||||
|
darkMode: ["class"],
|
||||||
|
|
||||||
|
// Tell Tailwind where to look for class usage
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
// Shadcn/ui uses CSS variables for colors - defined in globals.css
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Border radius also uses CSS variables for consistency
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
27
frontend/tsconfig.json
Normal file
27
frontend/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user