This commit is contained in:
2026-02-03 21:57:43 +01:00
parent c2afe84f35
commit 3019bcaf1a
29 changed files with 4094 additions and 0 deletions

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

34
frontend/package.json Normal file
View 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"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View 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));
}

View 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>
);
}

View 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>
);
}

View 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;
}
}

View 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>
);
}

View 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
View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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 };

View 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 };

View 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 };

View 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 };

View 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
View 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;
}
}

View 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);
}

View 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
View 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));
}

View 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
View 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"]
}