diff --git a/app/api/contacts/route.ts b/app/api/contacts/route.ts index 94dec66..a6693da 100644 --- a/app/api/contacts/route.ts +++ b/app/api/contacts/route.ts @@ -1,25 +1,137 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { type NextRequest, NextResponse } from "next/server"; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; +import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; import { prisma } from '@/lib/prisma'; export async function GET(request: NextRequest) { - try { - // In a real app, you would check for admin session here - // For now, we trust the 'x-admin-request' header if it's set by the server-side component or middleware - // but typically you'd verify the session cookie/token + try { + const { searchParams } = new URL(request.url); + const filter = searchParams.get('filter') || 'all'; + const limit = parseInt(searchParams.get('limit') || '50'); + const offset = parseInt(searchParams.get('offset') || '0'); - const contacts = await prisma.contact.findMany({ - orderBy: { - createdAt: 'desc', - }, - take: 100, - }); + let whereClause = {}; + + switch (filter) { + case 'unread': + whereClause = { responded: false }; + break; + case 'responded': + whereClause = { responded: true }; + break; + default: + whereClause = {}; + } - return NextResponse.json({ contacts }); - } catch (error) { - console.error('Error fetching contacts:', error); - return NextResponse.json( - { error: 'Failed to fetch contacts' }, - { status: 500 } - ); - } + const [contacts, total] = await Promise.all([ + prisma.contact.findMany({ + where: whereClause, + orderBy: { createdAt: 'desc' }, + take: limit, + skip: offset, + }), + prisma.contact.count({ where: whereClause }) + ]); + + return NextResponse.json({ + contacts, + total, + hasMore: offset + contacts.length < total + }); + + } catch (error) { + // Handle missing database table gracefully + if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { + if (process.env.NODE_ENV === 'development') { + console.warn('Contact table does not exist. Returning empty result.'); + } + return NextResponse.json({ + contacts: [], + total: 0, + hasMore: false + }); + } + + if (process.env.NODE_ENV === 'development') { + console.error('Error fetching contacts:', error); + } + return NextResponse.json( + { error: 'Failed to fetch contacts' }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + // Rate limiting for POST requests + const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; + if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute + return new NextResponse( + JSON.stringify({ error: 'Rate limit exceeded' }), + { + status: 429, + headers: { + 'Content-Type': 'application/json', + ...getRateLimitHeaders(ip, 5, 60000) + } + } + ); + } + + const body = await request.json(); + const { name, email, subject, message } = body; + + // Validate required fields + if (!name || !email || !subject || !message) { + return NextResponse.json( + { error: 'All fields are required' }, + { status: 400 } + ); + } + + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return NextResponse.json( + { error: 'Invalid email format' }, + { status: 400 } + ); + } + + const contact = await prisma.contact.create({ + data: { + name, + email, + subject, + message, + responded: false + } + }); + + return NextResponse.json({ + message: 'Contact created successfully', + contact + }, { status: 201 }); + + } catch (error) { + // Handle missing database table gracefully + if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { + if (process.env.NODE_ENV === 'development') { + console.warn('Contact table does not exist.'); + } + return NextResponse.json( + { error: 'Database table not found. Please run migrations.' }, + { status: 503 } + ); + } + + if (process.env.NODE_ENV === 'development') { + console.error('Error creating contact:', error); + } + return NextResponse.json( + { error: 'Failed to create contact' }, + { status: 500 } + ); + } } diff --git a/app/api/contacts/route.tsx b/app/api/contacts/route.tsx deleted file mode 100644 index d674293..0000000 --- a/app/api/contacts/route.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import { PrismaClient } from '@prisma/client'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; -import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; - -const prisma = new PrismaClient(); - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const filter = searchParams.get('filter') || 'all'; - const limit = parseInt(searchParams.get('limit') || '50'); - const offset = parseInt(searchParams.get('offset') || '0'); - - let whereClause = {}; - - switch (filter) { - case 'unread': - whereClause = { responded: false }; - break; - case 'responded': - whereClause = { responded: true }; - break; - default: - whereClause = {}; - } - - const [contacts, total] = await Promise.all([ - prisma.contact.findMany({ - where: whereClause, - orderBy: { createdAt: 'desc' }, - take: limit, - skip: offset, - }), - prisma.contact.count({ where: whereClause }) - ]); - - return NextResponse.json({ - contacts, - total, - hasMore: offset + contacts.length < total - }); - - } catch (error) { - // Handle missing database table gracefully - if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { - if (process.env.NODE_ENV === 'development') { - console.warn('Contact table does not exist. Returning empty result.'); - } - return NextResponse.json({ - contacts: [], - total: 0, - hasMore: false - }); - } - - if (process.env.NODE_ENV === 'development') { - console.error('Error fetching contacts:', error); - } - return NextResponse.json( - { error: 'Failed to fetch contacts' }, - { status: 500 } - ); - } -} - -export async function POST(request: NextRequest) { - try { - // Rate limiting for POST requests - const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; - if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute - return new NextResponse( - JSON.stringify({ error: 'Rate limit exceeded' }), - { - status: 429, - headers: { - 'Content-Type': 'application/json', - ...getRateLimitHeaders(ip, 5, 60000) - } - } - ); - } - - const body = await request.json(); - const { name, email, subject, message } = body; - - // Validate required fields - if (!name || !email || !subject || !message) { - return NextResponse.json( - { error: 'All fields are required' }, - { status: 400 } - ); - } - - // Validate email format - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - return NextResponse.json( - { error: 'Invalid email format' }, - { status: 400 } - ); - } - - const contact = await prisma.contact.create({ - data: { - name, - email, - subject, - message, - responded: false - } - }); - - return NextResponse.json({ - message: 'Contact created successfully', - contact - }, { status: 201 }); - - } catch (error) { - // Handle missing database table gracefully - if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { - if (process.env.NODE_ENV === 'development') { - console.warn('Contact table does not exist.'); - } - return NextResponse.json( - { error: 'Database table not found. Please run migrations.' }, - { status: 503 } - ); - } - - if (process.env.NODE_ENV === 'development') { - console.error('Error creating contact:', error); - } - return NextResponse.json( - { error: 'Failed to create contact' }, - { status: 500 } - ); - } -} diff --git a/app/components/KernelPanic404.tsx b/app/components/KernelPanic404.tsx index 1750a30..0707a76 100644 --- a/app/components/KernelPanic404.tsx +++ b/app/components/KernelPanic404.tsx @@ -8,6 +8,7 @@ export default function KernelPanic404() { const inputContainerRef = useRef(null); const overlayRef = useRef(null); const bodyRef = useRef(null); // We'll use a wrapper div instead of document.body for some effects if possible, but strict effects might need body. + const bootStartedRef = useRef(false); useEffect(() => { /* --- SYSTEM CORE --- */ @@ -20,6 +21,10 @@ export default function KernelPanic404() { const body = document.body; if (!output || !input || !inputContainer || !overlay) return; + + // Prevent double initialization - check if boot already started or output has content + if (bootStartedRef.current || output.children.length > 0) return; + bootStartedRef.current = true; let audioCtx: AudioContext | null = null; let systemFrozen = false; diff --git a/components/Toast.tsx b/components/Toast.tsx index 89187c3..51159e4 100644 --- a/components/Toast.tsx +++ b/components/Toast.tsx @@ -59,7 +59,7 @@ const ToastItem = ({ toast, onRemove }: ToastProps) => { const getColors = () => { switch (toast.type) { case 'success': - return 'bg-stone-50 border-green-200 text-green-800 shadow-md'; + return 'bg-stone-50 border-green-300 text-green-900 shadow-md'; case 'error': return 'bg-stone-50 border-red-200 text-red-800 shadow-md'; case 'warning': @@ -291,7 +291,7 @@ export const ToastProvider = ({ children }: { children: React.ReactNode }) => { {children} {/* Toast Container */} -
+
{toasts.map((toast) => (