mirror of
https://github.com/leonvanzyl/agentic-coding-starter-kit.git
synced 2026-02-01 07:03:36 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user