From 62f8a0ca352d463c66e29829b791b7db6e0f00e5 Mon Sep 17 00:00:00 2001 From: Riccardo Giorato Date: Thu, 16 Apr 2026 22:02:03 +0200 Subject: [PATCH] feat: add feedback system with modal, api endpoints, and stats tracking --- app/api/feedback/route.ts | 22 ++++++ app/api/stats/route.ts | 15 ++++ components/feedback-modal.tsx | 113 ++++++++++++++++++++++++++++ components/landing/footer.tsx | 75 +++++++++++------- components/landing/hero-section.tsx | 33 ++++++++ drizzle/0005_feedback.sql | 6 ++ lib/db-actions.ts | 18 ++++- lib/schema.ts | 13 +++- 8 files changed, 263 insertions(+), 32 deletions(-) create mode 100644 app/api/feedback/route.ts create mode 100644 app/api/stats/route.ts create mode 100644 components/feedback-modal.tsx create mode 100644 drizzle/0005_feedback.sql diff --git a/app/api/feedback/route.ts b/app/api/feedback/route.ts new file mode 100644 index 0000000..eb4dede --- /dev/null +++ b/app/api/feedback/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@clerk/nextjs/server'; +import { createFeedback } from '@/lib/db-actions'; + +export async function POST(request: NextRequest) { + const { userId } = await auth(); + + const body = await request.json(); + const message = body?.message?.trim(); + + if (!message || message.length === 0) { + return NextResponse.json({ error: 'Message is required' }, { status: 400 }); + } + + if (message.length > 2000) { + return NextResponse.json({ error: 'Message is too long' }, { status: 400 }); + } + + await createFeedback({ message, userId: userId ?? undefined }); + + return NextResponse.json({ success: true }); +} diff --git a/app/api/stats/route.ts b/app/api/stats/route.ts new file mode 100644 index 0000000..ef7dfe0 --- /dev/null +++ b/app/api/stats/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; +import { getPagesGeneratedLast24Hours } from '@/lib/db-actions'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 0; + +export async function GET() { + try { + const pagesLast24h = await getPagesGeneratedLast24Hours(); + return NextResponse.json({ pagesLast24h }); + } catch (error) { + console.error('Error fetching stats:', error); + return NextResponse.json({ pagesLast24h: 0 }, { status: 200 }); + } +} diff --git a/components/feedback-modal.tsx b/components/feedback-modal.tsx new file mode 100644 index 0000000..48d937c --- /dev/null +++ b/components/feedback-modal.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { useState } from "react"; +import { MessageSquare, ArrowRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Textarea } from "@/components/ui/textarea"; + +interface FeedbackModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function FeedbackModal({ isOpen, onClose }: FeedbackModalProps) { + const [message, setMessage] = useState(""); + const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!message.trim()) return; + + setStatus("loading"); + try { + const res = await fetch("/api/feedback", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message: message.trim() }), + }); + if (!res.ok) throw new Error(); + setStatus("success"); + setMessage(""); + } catch { + setStatus("error"); + } + }; + + const handleClose = () => { + setMessage(""); + setStatus("idle"); + onClose(); + }; + + return ( + + + +
+
+ +
+
+ + Share your feedback + + + What do you think? Any bugs, ideas, or feature requests are welcome. + +
+ + {status === "success" ? ( +
+

Thanks for your feedback!

+

We really appreciate it.

+ +
+ ) : ( +
+