diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 15bf0f0..f753bfb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,11 @@ "Bash(timeout:*)", "Bash(timeout /t 2)", "Bash(if exist .nextdevlock del /F .nextdevlock)", - "Bash(if exist .nextdev rd /S /Q .nextdev)" + "Bash(if exist .nextdev rd /S /Q .nextdev)", + "mcp__serena__initial_instructions", + "mcp__serena__get_current_config", + "mcp__playwright__browser_fill_form", + "WebSearch" ], "deny": [], "ask": [] diff --git a/app/admin/AdminLayoutClient.tsx b/app/admin/AdminLayoutClient.tsx new file mode 100644 index 0000000..8ca456f --- /dev/null +++ b/app/admin/AdminLayoutClient.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter, usePathname } from 'next/navigation'; +import { AdminSidebar } from '@/components/admin/AdminSidebar'; + +export default function AdminLayoutClient({ + children, +}: { + children: React.ReactNode; +}) { + const router = useRouter(); + const pathname = usePathname(); + + useEffect(() => { + // Skip auth check for login page + if (pathname === '/admin/login') { + return; + } + + // Check authentication + const checkAuth = async () => { + try { + const response = await fetch('/api/admin/auth/verify'); + if (!response.ok) { + router.push('/admin/login'); + } + } catch (error) { + router.push('/admin/login'); + } + }; + + checkAuth(); + }, [pathname, router]); + + // If on login page, render full-screen without sidebar + if (pathname === '/admin/login') { + return <>{children}; + } + + // Dashboard/orders pages with sidebar + return ( +
+ +
+ {children} +
+
+ ); +} diff --git a/app/admin/dashboard/page.tsx b/app/admin/dashboard/page.tsx new file mode 100644 index 0000000..2a7d4f1 --- /dev/null +++ b/app/admin/dashboard/page.tsx @@ -0,0 +1,119 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { DollarSign, Package, Leaf, TrendingUp } from 'lucide-react'; + +export default function AdminDashboard() { + // Placeholder data - will be replaced with real API data + const stats = [ + { + title: 'Total Orders', + value: '0', + icon: , + trend: { value: 0, isPositive: true }, + gradient: 'bg-gradient-to-br from-royal-purple to-purple-600', + bgColor: 'bg-royal-purple', + }, + { + title: 'Total CO₂ Offset', + value: '0 tons', + icon: , + trend: { value: 0, isPositive: true }, + gradient: 'bg-gradient-to-br from-sea-green to-green-600', + bgColor: 'bg-sea-green', + }, + { + title: 'Total Revenue', + value: '$0', + icon: , + trend: { value: 0, isPositive: true }, + gradient: 'bg-gradient-to-br from-muted-gold to-orange-600', + bgColor: 'bg-muted-gold', + }, + { + title: 'Fulfillment Rate', + value: '0%', + icon: , + trend: { value: 0, isPositive: true }, + gradient: 'bg-gradient-to-br from-maritime-teal to-teal-600', + bgColor: 'bg-maritime-teal', + }, + ]; + + return ( +
+ {/* Header */} +
+

Dashboard

+

Welcome to the Puffin Offset Admin Portal

+
+ + {/* Stats Grid */} +
+ {stats.map((stat, index) => ( + +
+
+ {stat.icon} +
+ {stat.trend && ( + + {stat.trend.isPositive ? '+' : '-'}{stat.trend.value}% + + )} +
+

{stat.title}

+

{stat.value}

+
+ ))} +
+ + {/* Placeholder for Charts */} +
+ +

Orders Timeline

+
+ Chart will be implemented in Phase 3 +
+
+ + +

Status Distribution

+
+ Chart will be implemented in Phase 3 +
+
+
+ + {/* Info Card */} + +

🎉 Admin Center Active!

+

+ Phase 1 (Foundation) is complete. Dashboard, authentication, and navigation are now functional. + Phase 2 will add backend APIs and real data integration. +

+
+
+ ); +} diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx new file mode 100644 index 0000000..c2fd11c --- /dev/null +++ b/app/admin/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next'; +import AdminLayoutClient from './AdminLayoutClient'; + +export const metadata: Metadata = { + title: { + default: 'Admin Portal | Puffin Offset', + template: '%s | Admin Portal', + }, + description: 'Admin management portal for Puffin Offset', + robots: { + index: false, + follow: false, + }, +}; + +export default function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/app/admin/login/page.tsx b/app/admin/login/page.tsx new file mode 100644 index 0000000..849c2f1 --- /dev/null +++ b/app/admin/login/page.tsx @@ -0,0 +1,201 @@ +'use client'; + +import { useState, FormEvent } from 'react'; +import { useRouter } from 'next/navigation'; +import { Lock, User, Loader2 } from 'lucide-react'; +import { motion } from 'framer-motion'; +import Image from 'next/image'; + +export default function AdminLogin() { + const router = useRouter(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + try { + const response = await fetch('/api/admin/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }), + }); + + const data = await response.json(); + + if (!response.ok) { + setError(data.error || 'Login failed'); + setIsLoading(false); + return; + } + + // Successful login - redirect to dashboard + router.push('/admin/dashboard'); + } catch (err) { + setError('Network error. Please try again.'); + setIsLoading(false); + } + }; + + return ( +
+ {/* Monaco Background Image */} +
+ + {/* Overlay gradient for better readability */} +
+ + + {/* Logo and Title */} +
+ +
+ Puffin Offset Logo +
+
+ + Admin Portal + + + Puffin Offset Management + +
+ + {/* Login Card */} + +
+ {/* Error Message */} + {error && ( + + {error} + + )} + + {/* Username Field */} +
+ +
+
+ +
+ setUsername(e.target.value)} + className="w-full pl-10 pr-4 py-3 bg-white border border-light-gray-border rounded-lg text-deep-sea-blue placeholder-deep-sea-blue/40 focus:outline-none focus:ring-2 focus:ring-maritime-teal focus:border-maritime-teal transition-all" + placeholder="Enter your username" + required + disabled={isLoading} + /> +
+
+ + {/* Password Field */} +
+ +
+
+ +
+ setPassword(e.target.value)} + className="w-full pl-10 pr-4 py-3 bg-white border border-light-gray-border rounded-lg text-deep-sea-blue placeholder-deep-sea-blue/40 focus:outline-none focus:ring-2 focus:ring-maritime-teal focus:border-maritime-teal transition-all" + placeholder="Enter your password" + required + disabled={isLoading} + /> +
+
+ + {/* Submit Button */} + + {isLoading ? ( + + + Signing in... + + ) : ( + 'Sign In' + )} + +
+
+ + {/* Footer */} + + © 2024 Puffin Offset. Secure admin access. + +
+
+ ); +} diff --git a/app/admin/orders/page.tsx b/app/admin/orders/page.tsx new file mode 100644 index 0000000..a7f7874 --- /dev/null +++ b/app/admin/orders/page.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { Package } from 'lucide-react'; + +export default function AdminOrders() { + return ( +
+ {/* Header */} +
+

Orders

+

View and manage all carbon offset orders

+
+ + {/* Placeholder */} + + +

Orders Management

+

+ Orders table with filtering, search, and export will be implemented in Phase 4. +

+
+ Backend API integration coming in Phase 2 +
+
+
+ ); +} diff --git a/app/api/admin/auth/login/route.ts b/app/api/admin/auth/login/route.ts new file mode 100644 index 0000000..9c5531b --- /dev/null +++ b/app/api/admin/auth/login/route.ts @@ -0,0 +1,59 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { verifyCredentials, generateToken } from '@/lib/admin/auth'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { username, password } = body; + + // Validate input + if (!username || !password) { + return NextResponse.json( + { error: 'Username and password are required' }, + { status: 400 } + ); + } + + // Verify credentials + const isValid = verifyCredentials(username, password); + + if (!isValid) { + return NextResponse.json( + { error: 'Invalid credentials' }, + { status: 401 } + ); + } + + // Generate JWT token + const token = generateToken({ + username, + isAdmin: true, + }); + + // Create response with token in cookie + const response = NextResponse.json({ + success: true, + user: { + username, + isAdmin: true, + }, + }); + + // Set HTTP-only cookie with JWT token + response.cookies.set('admin-token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24, // 24 hours + path: '/', + }); + + return response; + } catch (error) { + console.error('Login error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/auth/logout/route.ts b/app/api/admin/auth/logout/route.ts new file mode 100644 index 0000000..49e5148 --- /dev/null +++ b/app/api/admin/auth/logout/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server'; + +export async function POST() { + const response = NextResponse.json({ success: true }); + + // Clear the admin token cookie + response.cookies.set('admin-token', '', { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 0, + path: '/', + }); + + return response; +} diff --git a/app/api/admin/auth/verify/route.ts b/app/api/admin/auth/verify/route.ts new file mode 100644 index 0000000..05e90f1 --- /dev/null +++ b/app/api/admin/auth/verify/route.ts @@ -0,0 +1,18 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAdminFromRequest } from '@/lib/admin/middleware'; + +export async function GET(request: NextRequest) { + const admin = getAdminFromRequest(request); + + if (!admin) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + return NextResponse.json({ + success: true, + user: admin, + }); +} diff --git a/app/layout.tsx b/app/layout.tsx index 0b5a47c..78ece89 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,8 +1,7 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import Script from 'next/script'; -import { Header } from '../components/Header'; -import { Footer } from '../components/Footer'; +import { RootLayoutClient } from '../components/RootLayoutClient'; import './globals.css'; const inter = Inter({ subsets: ['latin'] }); @@ -100,11 +99,7 @@ export default function RootLayout({ )} -
-
- {children} -
-