Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Development Commands

- `pnpm dev` - Start development server with Turbopack for fast refresh
- `pnpm build` - Build the application for production
- `pnpm start` - Start the production server
- `pnpm lint` - Run ESLint to check code quality

## Architecture Overview

This is a Next.js 15 application built with TypeScript and Firebase, implementing a request management system with user authentication and admin functionality.

### Firebase Integration

- **Client-side**: Firebase v9 SDK (`src/lib/firebase.ts`) for Firestore database and client authentication
- **Server-side**: Firebase Admin SDK (`src/lib/admin.ts`) for server-side authentication and user management
- **Authentication**: Session-based auth using Firebase session cookies with server-side verification

### Key Architecture Patterns

**Server Actions**: All form submissions use Next.js server actions:

- `src/app/requests/new/actions.ts` - Creates new requests
- `src/app/promote/actions.ts` - Promotes users to admin role

**Authentication Flow**:

- `src/utils/server.ts` contains `identify()` function for server-side user verification
- `hold()` function creates secure session cookies from Firebase tokens
- Admin access controlled via Firebase custom claims

**Data Flow**:

- Requests are stored in Firestore with user association via `uid`
- User identification happens server-side before database operations
- Form data processing uses `shape()` utility from `src/utils/client.ts`

### Route Structure

- `/` - Home page
- `/login` & `/signup` - Authentication pages
- `/requests` - User's request listing (protected)
- `/requests/new` - Create new request form (protected)
- `/requests/[id]` - Individual request details (protected)
- `/review` - Admin review interface (admin-only)
- `/promote` - User promotion to admin (admin-only)

### Environment Configuration

- Requires `GOOGLE_APPLICATION_CREDENTIALS` environment variable pointing to Firebase service account JSON
- Firebase client config is hardcoded in `src/lib/firebase.ts`

### Styling

- Uses Tailwind CSS v4 with PostCSS
- Base styles in `src/styles/base.css`
- Inter font loaded via Next.js font optimization
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@
"firebase": "11.10.0",
"firebase-admin": "13.4.0",
"lucide-react": "0.525.0",
"next": "15.3.4",
"next": "15.3.5",
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@eslint/eslintrc": "3.3.1",
"@tailwindcss/postcss": "4.1.11",
"@types/node": "24.0.8",
"@types/node": "24.0.10",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"eslint": "9.30.0",
"eslint-config-next": "15.3.4",
"eslint": "9.30.1",
"eslint-config-next": "15.3.5",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.13",
"tailwindcss": "4.1.11",
Expand Down
434 changes: 217 additions & 217 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

53 changes: 50 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "@/styles/base.css"
import { Metadata } from "next"
import { Inter } from "next/font/google"
import { ReactNode } from "react"
import Link from "next/link"

const inter = Inter({ subsets: ["latin"] })

Expand All @@ -13,9 +14,55 @@ export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body className={`flex min-h-screen flex-col ${inter.className}`}>
<header></header>
<main className="grow">{children}</main>
<footer></footer>
<header className="page-header">
<div className="container">
<div className="page-title">
<div className="flex items-center justify-between">
<Link
href="/"
className="text-2xl font-bold text-slate-900 hover:no-underline"
>
Tefflon
</Link>
<nav className="flex items-center space-x-6">
<Link href="/" className="nav-link">
Home
</Link>
<Link href="/requests" className="nav-link">
Requests
</Link>
<Link href="/requests/new" className="nav-link">
New Request
</Link>
<Link href="/review" className="nav-link">
Review
</Link>
<Link href="/promote" className="nav-link">
Promote
</Link>
<Link href="/login" className="nav-link">
Login
</Link>
<Link href="/signup" className="nav-link">
Sign Up
</Link>
</nav>
</div>
</div>
</div>
</header>

<main className="grow">
<div className="main-content container">{children}</div>
</main>

<footer className="mt-auto border-t border-slate-200 bg-slate-100">
<div className="container py-8">
<p className="mb-0 text-center text-sm text-slate-600">
© {new Date().getFullYear()} Tefflon. All rights reserved.
</p>
</div>
</footer>
</body>
</html>
)
Expand Down
29 changes: 20 additions & 9 deletions src/app/login/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function Form() {

return (
<form
className="space-y-6"
action={async (fd) => {
const { email, password } = shape(fd)

Expand All @@ -26,15 +27,25 @@ export default function Form() {
}
}}
>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />

<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" required />

{error && <p>{error}</p>}

<Pending />
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input id="email" name="email" type="email" required />
</div>

<div className="form-group">
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" required />
</div>

{error && (
<div className="alert alert-error">
{error}
</div>
)}

<div className="pt-2">
<Pending fullWidth />
</div>
</form>
)
}
28 changes: 24 additions & 4 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import { Metadata } from "next"
import Link from "next/link"
import Form from "./form"

export const metadata: Metadata = { title: "Log In" }

export default function LogIn() {
return (
<>
<h1>Log In</h1>
<Form />
</>
<div className="max-w-md mx-auto">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-slate-900 mb-2">
Log In
</h1>
<p className="text-slate-600">
Access your account to manage your requests
</p>
</div>

<div className="card">
<Form />

<div className="mt-6 text-center">
<p className="text-slate-600">
Don't have an account?{" "}
<Link href="/signup" className="text-blue-600 hover:text-blue-700 font-medium">
Sign up
</Link>
</p>
</div>
</div>
</div>
)
}
76 changes: 73 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,77 @@
import Link from "next/link"

export default function Home() {
return (
<>
<h1>Home</h1>
</>
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-slate-900 mb-4">
Welcome to Tefflon
</h1>
<p className="text-xl text-slate-600 mb-8">
Your government request management system
</p>
</div>

<div className="grid md:grid-cols-2 gap-8 mb-12">
<div className="card">
<h2 className="text-2xl font-semibold text-slate-800 mb-4">
Submit a Request
</h2>
<p className="text-slate-600 mb-6">
Create and submit new requests for approval through our streamlined process.
</p>
<Link href="/requests/new" className="btn-primary">
New Request
</Link>
</div>

<div className="card">
<h2 className="text-2xl font-semibold text-slate-800 mb-4">
View Your Requests
</h2>
<p className="text-slate-600 mb-6">
Track the status and details of all your submitted requests.
</p>
<Link href="/requests" className="btn-primary">
View Requests
</Link>
</div>
</div>

<div className="card bg-blue-50 border-blue-200">
<h2 className="text-2xl font-semibold text-slate-800 mb-4">
Getting Started
</h2>
<div className="space-y-4">
<div className="flex items-start space-x-3">
<span className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-medium">
1
</span>
<div>
<h3 className="font-medium text-slate-900">Create an Account</h3>
<p className="text-slate-600">Sign up for a new account to get started.</p>
</div>
</div>
<div className="flex items-start space-x-3">
<span className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-medium">
2
</span>
<div>
<h3 className="font-medium text-slate-900">Submit Your Request</h3>
<p className="text-slate-600">Fill out the request form with all required details.</p>
</div>
</div>
<div className="flex items-start space-x-3">
<span className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-medium">
3
</span>
<div>
<h3 className="font-medium text-slate-900">Track Progress</h3>
<p className="text-slate-600">Monitor your request status and receive updates.</p>
</div>
</div>
</div>
</div>
</div>
)
}
40 changes: 29 additions & 11 deletions src/app/promote/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,37 @@ export default function Form() {
const [state, action] = useActionState(promoteUser, undefined)

return (
<form action={action}>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
required
defaultValue={state && (state.fd.get("email") as string)}
/>
<form action={action} className="space-y-6">
<div className="form-group">
<label htmlFor="email">User Email Address</label>
<input
id="email"
name="email"
type="email"
required
defaultValue={state && (state.fd.get("email") as string)}
placeholder="user@example.com"
/>
<p className="form-help">
Enter the email address of the user you want to promote to admin
</p>
</div>

{state && <p>{state.error}</p>}
{state?.error && (
<div className="alert alert-error">
{state.error}
</div>
)}

<Pending />
{!state?.error && state && (
<div className="alert alert-success">
User has been successfully promoted to admin.
</div>
)}

<div className="pt-2">
<Pending text="Promote to Admin" />
</div>
</form>
)
}
Loading