feat: comprehensive boilerplate improvements

Security & Stability:
- Add Next.js 16 proxy.ts for BetterAuth cookie-based auth protection
- Add rate limiting for API routes (src/lib/rate-limit.ts)
- Add Zod validation for chat API request bodies
- Add session auth check to chat and diagnostics endpoints
- Add security headers in next.config.ts (CSP, X-Frame-Options, etc.)
- Add file upload validation and sanitization in storage.ts

Core UX Components:
- Add error boundaries (error.tsx, not-found.tsx, chat/error.tsx)
- Add loading states (skeleton.tsx, spinner.tsx, loading.tsx files)
- Add toast notifications with Sonner
- Add form components (input.tsx, textarea.tsx, label.tsx)
- Add database indexes for performance (schema.ts)
- Enhance chat UX: timestamps, copy-to-clipboard, thinking indicator,
  error display, localStorage message persistence

Polish & Accessibility:
- Add Open Graph and Twitter card metadata
- Add JSON-LD structured data for SEO
- Add sitemap.ts, robots.ts, manifest.ts
- Add skip-to-content link and ARIA labels in site-header
- Enable profile page quick action buttons with dialogs
- Update Next.js 15 references to Next.js 16

Developer Experience:
- Add GitHub Actions CI workflow (lint, typecheck, build)
- Add Prettier configuration (.prettierrc, .prettierignore)
- Add .nvmrc pinning Node 20
- Add ESLint rules: import/order, react-hooks/exhaustive-deps
- Add stricter TypeScript settings (exactOptionalPropertyTypes,
  noImplicitOverride)
- Add interactive setup script (scripts/setup.ts)
- Add session utility functions (src/lib/session.ts)

All changes mirrored to create-agentic-app/template/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Leon van Zyl
2025-11-30 14:46:15 +02:00
parent 1121258238
commit a3a151c67a
125 changed files with 5088 additions and 493 deletions

View File

@@ -1,17 +1,37 @@
"use client";
import { useSession } from "@/lib/auth-client";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Mail, Calendar, User, Shield, ArrowLeft } from "lucide-react";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Mail, Calendar, User, Shield, ArrowLeft, Lock, Smartphone } from "lucide-react";
import { toast } from "sonner";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { useSession } from "@/lib/auth-client";
export default function ProfilePage() {
const { data: session, isPending } = useSession();
const router = useRouter();
const [editProfileOpen, setEditProfileOpen] = useState(false);
const [securityOpen, setSecurityOpen] = useState(false);
const [emailPrefsOpen, setEmailPrefsOpen] = useState(false);
if (isPending) {
return (
@@ -27,11 +47,20 @@ export default function ProfilePage() {
}
const user = session.user;
const createdDate = user.createdAt ? new Date(user.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}) : null;
const createdDate = user.createdAt
? new Date(user.createdAt).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
: null;
const handleEditProfileSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// In a real app, this would call an API to update the user profile
toast.info("Profile updates require backend implementation");
setEditProfileOpen(false);
};
return (
<div className="container max-w-4xl mx-auto py-8 px-4">
@@ -60,11 +89,7 @@ export default function ProfilePage() {
referrerPolicy="no-referrer"
/>
<AvatarFallback className="text-lg">
{(
user.name?.[0] ||
user.email?.[0] ||
"U"
).toUpperCase()}
{(user.name?.[0] || user.email?.[0] || "U").toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="space-y-2">
@@ -73,7 +98,10 @@ export default function ProfilePage() {
<Mail className="h-4 w-4" />
<span>{user.email}</span>
{user.emailVerified && (
<Badge variant="outline" className="text-green-600 border-green-600">
<Badge
variant="outline"
className="text-green-600 border-green-600"
>
<Shield className="h-3 w-3 mr-1" />
Verified
</Badge>
@@ -94,9 +122,7 @@ export default function ProfilePage() {
<Card>
<CardHeader>
<CardTitle>Account Information</CardTitle>
<CardDescription>
Your account details and settings
</CardDescription>
<CardDescription>Your account details and settings</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
@@ -115,16 +141,19 @@ export default function ProfilePage() {
<div className="p-3 border rounded-md bg-muted/10 flex items-center justify-between">
<span>{user.email}</span>
{user.emailVerified && (
<Badge variant="outline" className="text-green-600 border-green-600">
<Badge
variant="outline"
className="text-green-600 border-green-600"
>
Verified
</Badge>
)}
</div>
</div>
</div>
<Separator />
<div className="space-y-4">
<h3 className="text-lg font-medium">Account Status</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -171,7 +200,10 @@ export default function ProfilePage() {
<p className="text-sm text-muted-foreground">Active now</p>
</div>
</div>
<Badge variant="outline" className="text-green-600 border-green-600">
<Badge
variant="outline"
className="text-green-600 border-green-600"
>
Active
</Badge>
</div>
@@ -189,34 +221,195 @@ export default function ProfilePage() {
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Button variant="outline" className="justify-start h-auto p-4" disabled>
<Button
variant="outline"
className="justify-start h-auto p-4"
onClick={() => setEditProfileOpen(true)}
>
<User className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Edit Profile</div>
<div className="text-xs text-muted-foreground">Update your information</div>
<div className="text-xs text-muted-foreground">
Update your information
</div>
</div>
</Button>
<Button variant="outline" className="justify-start h-auto p-4" disabled>
<Button
variant="outline"
className="justify-start h-auto p-4"
onClick={() => setSecurityOpen(true)}
>
<Shield className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Security Settings</div>
<div className="text-xs text-muted-foreground">Manage security options</div>
<div className="text-xs text-muted-foreground">
Manage security options
</div>
</div>
</Button>
<Button variant="outline" className="justify-start h-auto p-4" disabled>
<Button
variant="outline"
className="justify-start h-auto p-4"
onClick={() => setEmailPrefsOpen(true)}
>
<Mail className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Email Preferences</div>
<div className="text-xs text-muted-foreground">Configure notifications</div>
<div className="text-xs text-muted-foreground">
Configure notifications
</div>
</div>
</Button>
</div>
<p className="text-xs text-muted-foreground mt-4">
Additional profile management features coming soon.
</p>
</CardContent>
</Card>
</div>
{/* Edit Profile Dialog */}
<Dialog open={editProfileOpen} onOpenChange={setEditProfileOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Update your profile information. Changes will be saved to your
account.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleEditProfileSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
defaultValue={user.name || ""}
placeholder="Enter your name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
defaultValue={user.email || ""}
disabled
className="bg-muted"
/>
<p className="text-xs text-muted-foreground">
Email cannot be changed for OAuth accounts
</p>
</div>
<div className="flex justify-end gap-2 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setEditProfileOpen(false)}
>
Cancel
</Button>
<Button type="submit">Save Changes</Button>
</div>
</form>
</DialogContent>
</Dialog>
{/* Security Settings Dialog */}
<Dialog open={securityOpen} onOpenChange={setSecurityOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Security Settings</DialogTitle>
<DialogDescription>
Manage your account security and authentication options.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
<Lock className="h-5 w-5 text-muted-foreground" />
<div>
<p className="font-medium">Password</p>
<p className="text-sm text-muted-foreground">
{user.email?.includes("@gmail")
? "Managed by Google"
: "Set a password for your account"}
</p>
</div>
</div>
<Badge variant="outline">
{user.email?.includes("@gmail") ? "OAuth" : "Not Set"}
</Badge>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
<Smartphone className="h-5 w-5 text-muted-foreground" />
<div>
<p className="font-medium">Two-Factor Authentication</p>
<p className="text-sm text-muted-foreground">
Add an extra layer of security
</p>
</div>
</div>
<Button variant="outline" size="sm" disabled>
Coming Soon
</Button>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
<Shield className="h-5 w-5 text-muted-foreground" />
<div>
<p className="font-medium">Active Sessions</p>
<p className="text-sm text-muted-foreground">
Manage devices logged into your account
</p>
</div>
</div>
<Badge variant="default">1 Active</Badge>
</div>
</div>
<div className="flex justify-end pt-4">
<Button variant="outline" onClick={() => setSecurityOpen(false)}>
Close
</Button>
</div>
</DialogContent>
</Dialog>
{/* Email Preferences Dialog */}
<Dialog open={emailPrefsOpen} onOpenChange={setEmailPrefsOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Email Preferences</DialogTitle>
<DialogDescription>
Configure your email notification settings.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium">Marketing Emails</p>
<p className="text-sm text-muted-foreground">
Product updates and announcements
</p>
</div>
<Badge variant="secondary">Coming Soon</Badge>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium">Security Alerts</p>
<p className="text-sm text-muted-foreground">
Important security notifications
</p>
</div>
<Badge variant="default">Always On</Badge>
</div>
</div>
<div className="flex justify-end pt-4">
<Button variant="outline" onClick={() => setEmailPrefsOpen(false)}>
Close
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}
}