Refactor useAuth hook, settings components, and docs for formatting and readability improvements

- Consolidated multi-line arguments into single lines where appropriate in `useAuth`.
- Improved spacing and readability in data processing across components (`ProfileSettingsForm`, `PasswordChangeForm`, `SessionCard`).
- Applied consistent table and markdown formatting in design system docs (e.g., `README.md`, `08-ai-guidelines.md`, `00-quick-start.md`).
- Updated code snippets to ensure adherence to Prettier rules and streamlined JSX structures.
This commit is contained in:
2025-11-10 11:03:45 +01:00
parent 464a6140c4
commit 96df7edf88
208 changed files with 4056 additions and 4556 deletions

View File

@@ -5,10 +5,6 @@ export const metadata: Metadata = {
title: 'Authentication',
};
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return <AuthLayoutClient>{children}</AuthLayoutClient>;
}

View File

@@ -19,18 +19,13 @@ export default function LoginPage() {
return (
<div className="space-y-6">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight">
Sign in to your account
</h2>
<h2 className="text-3xl font-bold tracking-tight">Sign in to your account</h2>
<p className="mt-2 text-sm text-muted-foreground">
Access your dashboard and manage your account
</p>
</div>
<LoginForm
showRegisterLink
showPasswordResetLink
/>
<LoginForm showRegisterLink showPasswordResetLink />
</div>
);
}

View File

@@ -14,7 +14,10 @@ import Link from 'next/link';
// Code-split PasswordResetConfirmForm (319 lines)
const PasswordResetConfirmForm = dynamic(
/* istanbul ignore next - Next.js dynamic import, tested via component */
() => import('@/components/auth/PasswordResetConfirmForm').then((mod) => ({ default: mod.PasswordResetConfirmForm })),
() =>
import('@/components/auth/PasswordResetConfirmForm').then((mod) => ({
default: mod.PasswordResetConfirmForm,
})),
{
loading: () => (
<div className="space-y-4">
@@ -53,15 +56,12 @@ export default function PasswordResetConfirmContent() {
return (
<div className="space-y-6">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight">
Invalid Reset Link
</h2>
<h2 className="text-3xl font-bold tracking-tight">Invalid Reset Link</h2>
</div>
<Alert variant="destructive">
<p className="text-sm">
This password reset link is invalid or has expired. Please request a new
password reset.
This password reset link is invalid or has expired. Please request a new password reset.
</p>
</Alert>

View File

@@ -8,16 +8,16 @@ import PasswordResetConfirmContent from './PasswordResetConfirmContent';
export default function PasswordResetConfirmPage() {
return (
<Suspense fallback={
<div className="space-y-6">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight">Set new password</h2>
<p className="mt-2 text-sm text-muted-foreground">
Loading...
</p>
<Suspense
fallback={
<div className="space-y-6">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight">Set new password</h2>
<p className="mt-2 text-sm text-muted-foreground">Loading...</p>
</div>
</div>
</div>
}>
}
>
<PasswordResetConfirmContent />
</Suspense>
);

View File

@@ -8,9 +8,10 @@ import dynamic from 'next/dynamic';
// Code-split PasswordResetRequestForm
const PasswordResetRequestForm = dynamic(
/* istanbul ignore next - Next.js dynamic import, tested via component */
() => import('@/components/auth/PasswordResetRequestForm').then((mod) => ({
default: mod.PasswordResetRequestForm
})),
() =>
import('@/components/auth/PasswordResetRequestForm').then((mod) => ({
default: mod.PasswordResetRequestForm,
})),
{
loading: () => (
<div className="space-y-4">
@@ -25,9 +26,7 @@ export default function PasswordResetPage() {
return (
<div className="space-y-6">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight">
Reset your password
</h2>
<h2 className="text-3xl font-bold tracking-tight">Reset your password</h2>
<p className="mt-2 text-muted-foreground">
We&apos;ll send you an email with instructions to reset your password
</p>

View File

@@ -19,9 +19,7 @@ export default function RegisterPage() {
return (
<div className="space-y-6">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight">
Create your account
</h2>
<h2 className="text-3xl font-bold tracking-tight">Create your account</h2>
<p className="mt-2 text-sm text-muted-foreground">
Get started with your free account today
</p>

View File

@@ -15,18 +15,12 @@ export const metadata: Metadata = {
},
};
export default function AuthenticatedLayout({
children,
}: {
children: React.ReactNode;
}) {
export default function AuthenticatedLayout({ children }: { children: React.ReactNode }) {
return (
<AuthGuard>
<div className="flex min-h-screen flex-col">
<Header />
<main className="flex-1">
{children}
</main>
<main className="flex-1">{children}</main>
<Footer />
</div>
</AuthGuard>

View File

@@ -40,11 +40,7 @@ const settingsTabs = [
},
];
export default function SettingsLayout({
children,
}: {
children: React.ReactNode;
}) {
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Determine active tab based on pathname
@@ -54,12 +50,8 @@ export default function SettingsLayout({
<div className="container mx-auto px-4 py-8">
{/* Page Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-foreground">
Settings
</h1>
<p className="mt-2 text-muted-foreground">
Manage your account settings and preferences
</p>
<h1 className="text-3xl font-bold text-foreground">Settings</h1>
<p className="mt-2 text-muted-foreground">Manage your account settings and preferences</p>
</div>
{/* Tabs Navigation */}
@@ -79,9 +71,7 @@ export default function SettingsLayout({
</TabsList>
{/* Tab Content */}
<div className="rounded-lg border bg-card text-card-foreground p-6">
{children}
</div>
<div className="rounded-lg border bg-card text-card-foreground p-6">{children}</div>
</Tabs>
</div>
);

View File

@@ -11,9 +11,7 @@ export default function PasswordSettingsPage() {
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold text-foreground">
Password Settings
</h2>
<h2 className="text-2xl font-semibold text-foreground">Password Settings</h2>
<p className="text-muted-foreground mt-1">
Change your password to keep your account secure
</p>

View File

@@ -4,7 +4,7 @@
*/
/* istanbul ignore next - Next.js type import for metadata */
import type { Metadata} from 'next';
import type { Metadata } from 'next';
/* istanbul ignore next - Next.js metadata, not executable code */
export const metadata: Metadata = {
@@ -14,12 +14,8 @@ export const metadata: Metadata = {
export default function PreferencesPage() {
return (
<div>
<h2 className="text-2xl font-semibold text-foreground mb-4">
Preferences
</h2>
<p className="text-muted-foreground">
Configure your preferences (Coming in Task 3.5)
</p>
<h2 className="text-2xl font-semibold text-foreground mb-4">Preferences</h2>
<p className="text-muted-foreground">Configure your preferences (Coming in Task 3.5)</p>
</div>
);
}

View File

@@ -11,12 +11,8 @@ export default function ProfileSettingsPage() {
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold text-foreground">
Profile Settings
</h2>
<p className="text-muted-foreground mt-1">
Manage your profile information
</p>
<h2 className="text-2xl font-semibold text-foreground">Profile Settings</h2>
<p className="text-muted-foreground mt-1">Manage your profile information</p>
</div>
<ProfileSettingsForm />

View File

@@ -11,9 +11,7 @@ export default function SessionsPage() {
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold text-foreground">
Active Sessions
</h2>
<h2 className="text-2xl font-semibold text-foreground">Active Sessions</h2>
<p className="text-muted-foreground mt-1">
View and manage devices signed in to your account
</p>

View File

@@ -17,11 +17,7 @@ export const metadata: Metadata = {
},
};
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
export default function AdminLayout({ children }: { children: React.ReactNode }) {
return (
<AuthGuard requireAdmin>
<a

View File

@@ -27,9 +27,7 @@ export default function AdminPage() {
<div className="space-y-8">
{/* Page Header */}
<div>
<h1 className="text-3xl font-bold tracking-tight">
Admin Dashboard
</h1>
<h1 className="text-3xl font-bold tracking-tight">Admin Dashboard</h1>
<p className="mt-2 text-muted-foreground">
Manage users, organizations, and system settings
</p>
@@ -72,9 +70,7 @@ export default function AdminPage() {
<Settings className="h-5 w-5 text-primary" aria-hidden="true" />
<h3 className="font-semibold">System Settings</h3>
</div>
<p className="text-sm text-muted-foreground">
Configure system-wide settings
</p>
<p className="text-sm text-muted-foreground">Configure system-wide settings</p>
</div>
</Link>
</div>

View File

@@ -27,9 +27,7 @@ export default function AdminSettingsPage() {
</Button>
</Link>
<div>
<h1 className="text-3xl font-bold tracking-tight">
System Settings
</h1>
<h1 className="text-3xl font-bold tracking-tight">System Settings</h1>
<p className="mt-2 text-muted-foreground">
Configure system-wide settings and preferences
</p>
@@ -38,16 +36,12 @@ export default function AdminSettingsPage() {
{/* Placeholder Content */}
<div className="rounded-lg border bg-card p-12 text-center">
<h3 className="text-xl font-semibold mb-2">
System Settings Coming Soon
</h3>
<h3 className="text-xl font-semibold mb-2">System Settings Coming Soon</h3>
<p className="text-muted-foreground max-w-md mx-auto">
This page will allow you to configure system-wide settings,
preferences, and advanced options.
</p>
<p className="text-sm text-muted-foreground mt-4">
Features will include:
This page will allow you to configure system-wide settings, preferences, and advanced
options.
</p>
<p className="text-sm text-muted-foreground mt-4">Features will include:</p>
<ul className="text-sm text-muted-foreground mt-2 max-w-sm mx-auto text-left">
<li> General system configuration</li>
<li> Email and notification settings</li>

View File

@@ -28,12 +28,8 @@ export default function AdminUsersPage() {
</Button>
</Link>
<div>
<h1 className="text-3xl font-bold tracking-tight">
User Management
</h1>
<p className="mt-2 text-muted-foreground">
View, create, and manage user accounts
</p>
<h1 className="text-3xl font-bold tracking-tight">User Management</h1>
<p className="mt-2 text-muted-foreground">View, create, and manage user accounts</p>
</div>
</div>

View File

@@ -11,7 +11,9 @@ import dynamic from 'next/dynamic';
const ComponentShowcase = dynamic(
() => import('@/components/dev/ComponentShowcase').then((mod) => mod.ComponentShowcase),
{
loading: () => <div className="p-8 text-center text-muted-foreground">Loading components...</div>,
loading: () => (
<div className="p-8 text-center text-muted-foreground">Loading components...</div>
),
}
);

View File

@@ -21,9 +21,9 @@ export async function generateStaticParams() {
try {
const files = await fs.readdir(docsDir);
const mdFiles = files.filter(file => file.endsWith('.md'));
const mdFiles = files.filter((file) => file.endsWith('.md'));
return mdFiles.map(file => ({
return mdFiles.map((file) => ({
slug: [file.replace(/\.md$/, '')],
}));
} catch {
@@ -63,12 +63,7 @@ export default async function DocPage({ params }: DocPageProps) {
return (
<div className="bg-background">
{/* Breadcrumbs */}
<DevBreadcrumbs
items={[
{ label: 'Documentation', href: '/dev/docs' },
{ label: title }
]}
/>
<DevBreadcrumbs items={[{ label: 'Documentation', href: '/dev/docs' }, { label: title }]} />
<div className="container mx-auto px-4 py-12">
<div className="mx-auto max-w-4xl">

View File

@@ -5,7 +5,17 @@
*/
import Link from 'next/link';
import { BookOpen, Sparkles, Layout, Palette, Code2, FileCode, Accessibility, Lightbulb, Search } from 'lucide-react';
import {
BookOpen,
Sparkles,
Layout,
Palette,
Code2,
FileCode,
Accessibility,
Lightbulb,
Search,
} from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -106,9 +116,7 @@ export default function DocsHub() {
<section className="border-b bg-gradient-to-b from-background to-muted/20 py-12">
<div className="container mx-auto px-4">
<div className="mx-auto max-w-3xl text-center">
<h2 className="text-4xl font-bold tracking-tight mb-4">
Design System Documentation
</h2>
<h2 className="text-4xl font-bold tracking-tight mb-4">Design System Documentation</h2>
<p className="text-lg text-muted-foreground mb-8">
Comprehensive guides, best practices, and references for building consistent,
accessible, and maintainable user interfaces with the FastNext design system.
@@ -170,9 +178,7 @@ export default function DocsHub() {
</div>
</CardHeader>
<CardContent>
<CardDescription className="text-base">
{doc.description}
</CardDescription>
<CardDescription className="text-base">{doc.description}</CardDescription>
</CardContent>
</Card>
</Link>
@@ -203,9 +209,7 @@ export default function DocsHub() {
</div>
</CardHeader>
<CardContent>
<CardDescription>
{doc.description}
</CardDescription>
<CardDescription>{doc.description}</CardDescription>
</CardContent>
</Card>
</Link>
@@ -243,9 +247,7 @@ export default function DocsHub() {
</div>
</CardHeader>
<CardContent>
<CardDescription className="text-base">
{doc.description}
</CardDescription>
<CardDescription className="text-base">{doc.description}</CardDescription>
</CardContent>
</Card>
</Link>
@@ -254,7 +256,6 @@ export default function DocsHub() {
</section>
</div>
</main>
</div>
);
}

View File

@@ -14,12 +14,7 @@ import { z } from 'zod';
import { CheckCircle2, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { DevBreadcrumbs } from '@/components/dev/DevBreadcrumbs';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
@@ -109,9 +104,8 @@ export default function FormsPage() {
{/* Introduction */}
<div className="max-w-3xl space-y-4">
<p className="text-muted-foreground">
Complete form implementations using react-hook-form for state management
and Zod for validation. Includes error handling, loading states, and
accessibility features.
Complete form implementations using react-hook-form for state management and Zod for
validation. Includes error handling, loading states, and accessibility features.
</p>
<div className="flex flex-wrap gap-2">
<Badge variant="outline">react-hook-form</Badge>
@@ -170,16 +164,10 @@ const { register, handleSubmit, formState: { errors } } = useForm({
placeholder="you@example.com"
{...registerLogin('email')}
aria-invalid={!!errorsLogin.email}
aria-describedby={
errorsLogin.email ? 'login-email-error' : undefined
}
aria-describedby={errorsLogin.email ? 'login-email-error' : undefined}
/>
{errorsLogin.email && (
<p
id="login-email-error"
className="text-sm text-destructive"
role="alert"
>
<p id="login-email-error" className="text-sm text-destructive" role="alert">
{errorsLogin.email.message}
</p>
)}
@@ -194,9 +182,7 @@ const { register, handleSubmit, formState: { errors } } = useForm({
placeholder="••••••••"
{...registerLogin('password')}
aria-invalid={!!errorsLogin.password}
aria-describedby={
errorsLogin.password ? 'login-password-error' : undefined
}
aria-describedby={errorsLogin.password ? 'login-password-error' : undefined}
/>
{errorsLogin.password && (
<p
@@ -277,10 +263,7 @@ const { register, handleSubmit, formState: { errors } } = useForm({
</form>`}
>
<div className="max-w-md mx-auto">
<form
onSubmit={handleSubmitContact(onSubmitContact)}
className="space-y-4"
>
<form onSubmit={handleSubmitContact(onSubmitContact)} className="space-y-4">
{/* Name */}
<div className="space-y-2">
<Label htmlFor="contact-name">Name</Label>
@@ -317,9 +300,7 @@ const { register, handleSubmit, formState: { errors } } = useForm({
{/* Category */}
<div className="space-y-2">
<Label htmlFor="contact-category">Category</Label>
<Select
onValueChange={(value) => setValueContact('category', value)}
>
<Select onValueChange={(value) => setValueContact('category', value)}>
<SelectTrigger id="contact-category">
<SelectValue placeholder="Select a category" />
</SelectTrigger>
@@ -364,9 +345,7 @@ const { register, handleSubmit, formState: { errors } } = useForm({
<Alert className="border-green-500 text-green-600 dark:border-green-400 dark:text-green-400">
<CheckCircle2 className="h-4 w-4" />
<AlertTitle>Success!</AlertTitle>
<AlertDescription>
Your message has been sent successfully.
</AlertDescription>
<AlertDescription>Your message has been sent successfully.</AlertDescription>
</Alert>
)}
</form>
@@ -384,7 +363,7 @@ const { register, handleSubmit, formState: { errors } } = useForm({
title="Error State Best Practices"
description="Use aria-invalid and aria-describedby for accessibility"
before={{
caption: "No ARIA attributes, poor accessibility",
caption: 'No ARIA attributes, poor accessibility',
content: (
<div className="space-y-2 rounded-lg border p-4">
<div className="text-sm font-medium">Email</div>
@@ -394,7 +373,7 @@ const { register, handleSubmit, formState: { errors } } = useForm({
),
}}
after={{
caption: "With ARIA, screen reader accessible",
caption: 'With ARIA, screen reader accessible',
content: (
<div className="space-y-2 rounded-lg border p-4">
<div className="text-sm font-medium">Email</div>
@@ -422,15 +401,21 @@ const { register, handleSubmit, formState: { errors } } = useForm({
<ul className="space-y-2 text-sm">
<li className="flex items-start gap-2">
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5" />
<span>Add <code className="text-xs">aria-invalid=true</code> to invalid inputs</span>
<span>
Add <code className="text-xs">aria-invalid=true</code> to invalid inputs
</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5" />
<span>Link errors with <code className="text-xs">aria-describedby</code></span>
<span>
Link errors with <code className="text-xs">aria-describedby</code>
</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5" />
<span>Add <code className="text-xs">role=&quot;alert&quot;</code> to error messages</span>
<span>
Add <code className="text-xs">role=&quot;alert&quot;</code> to error messages
</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5" />
@@ -536,9 +521,7 @@ const { register, handleSubmit, formState: { errors } } = useForm({
<div className="space-y-2">
<div className="font-medium text-sm">Optional Field</div>
<code className="block rounded bg-muted p-2 text-xs">
z.string().optional()
</code>
<code className="block rounded bg-muted p-2 text-xs">z.string().optional()</code>
</div>
<div className="space-y-2">

View File

@@ -9,13 +9,7 @@ import Link from 'next/link';
import { Grid3x3 } from 'lucide-react';
import { DevBreadcrumbs } from '@/components/dev/DevBreadcrumbs';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Example, ExampleSection } from '@/components/dev/Example';
import { BeforeAfter } from '@/components/dev/BeforeAfter';
@@ -37,9 +31,8 @@ export default function LayoutsPage() {
{/* Introduction */}
<div className="max-w-3xl space-y-4">
<p className="text-muted-foreground">
These 5 essential layout patterns cover 80% of interface needs. Each
pattern includes live examples, before/after comparisons, and copy-paste
code.
These 5 essential layout patterns cover 80% of interface needs. Each pattern includes
live examples, before/after comparisons, and copy-paste code.
</p>
<div className="flex flex-wrap gap-2">
<Badge variant="outline">Grid vs Flex</Badge>
@@ -79,14 +72,12 @@ export default function LayoutsPage() {
<Card>
<CardHeader>
<CardTitle>Content Card</CardTitle>
<CardDescription>
Constrained to max-w-4xl for readability
</CardDescription>
<CardDescription>Constrained to max-w-4xl for readability</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Your main content goes here. The max-w-4xl constraint
ensures comfortable reading width.
Your main content goes here. The max-w-4xl constraint ensures comfortable
reading width.
</p>
</CardContent>
</Card>
@@ -99,25 +90,24 @@ export default function LayoutsPage() {
title="Common Mistake: No Width Constraint"
description="Content should not span full viewport width"
before={{
caption: "No max-width, hard to read on wide screens",
caption: 'No max-width, hard to read on wide screens',
content: (
<div className="w-full space-y-4 bg-background p-4 rounded">
<h3 className="font-semibold">Full Width Content</h3>
<p className="text-sm text-muted-foreground">
This text spans the entire width, making it hard to read on
large screens. Lines become too long.
This text spans the entire width, making it hard to read on large screens.
Lines become too long.
</p>
</div>
),
}}
after={{
caption: "Constrained with max-w for better readability",
caption: 'Constrained with max-w for better readability',
content: (
<div className="max-w-2xl mx-auto space-y-4 bg-background p-4 rounded">
<h3 className="font-semibold">Constrained Content</h3>
<p className="text-sm text-muted-foreground">
This text has a max-width, creating comfortable line lengths
for reading.
This text has a max-width, creating comfortable line lengths for reading.
</p>
</div>
),
@@ -149,14 +139,10 @@ export default function LayoutsPage() {
{[1, 2, 3, 4, 5, 6].map((i) => (
<Card key={i}>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">
Metric {i}
</CardTitle>
<CardTitle className="text-sm font-medium">Metric {i}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{(Math.random() * 1000).toFixed(0)}
</div>
<div className="text-2xl font-bold">{(Math.random() * 1000).toFixed(0)}</div>
<p className="text-xs text-muted-foreground mt-1">
+{(Math.random() * 20).toFixed(1)}% from last month
</p>
@@ -170,7 +156,7 @@ export default function LayoutsPage() {
title="Grid vs Flex for Equal Columns"
description="Use Grid for equal-width columns, not Flex"
before={{
caption: "flex with flex-1 - uneven wrapping",
caption: 'flex with flex-1 - uneven wrapping',
content: (
<div className="flex flex-wrap gap-4">
<div className="flex-1 min-w-[200px] rounded border bg-background p-4">
@@ -186,7 +172,7 @@ export default function LayoutsPage() {
),
}}
after={{
caption: "grid with grid-cols - consistent sizing",
caption: 'grid with grid-cols - consistent sizing',
content: (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="rounded border bg-background p-4">
@@ -231,9 +217,7 @@ export default function LayoutsPage() {
<Card className="max-w-md mx-auto">
<CardHeader>
<CardTitle>Login</CardTitle>
<CardDescription>
Enter your credentials to continue
</CardDescription>
<CardDescription>Enter your credentials to continue</CardDescription>
</CardHeader>
<CardContent>
<form className="space-y-4">
@@ -288,10 +272,7 @@ export default function LayoutsPage() {
</CardHeader>
<CardContent className="space-y-2">
{['Dashboard', 'Settings', 'Profile'].map((item) => (
<div
key={item}
className="rounded-md bg-muted px-3 py-2 text-sm"
>
<div key={item} className="rounded-md bg-muted px-3 py-2 text-sm">
{item}
</div>
))}
@@ -304,14 +285,12 @@ export default function LayoutsPage() {
<Card>
<CardHeader>
<CardTitle>Main Content</CardTitle>
<CardDescription>
Fixed 240px sidebar, fluid main area
</CardDescription>
<CardDescription>Fixed 240px sidebar, fluid main area</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
The sidebar remains 240px wide while the main content area
flexes to fill remaining space.
The sidebar remains 240px wide while the main content area flexes to fill
remaining space.
</p>
</CardContent>
</Card>
@@ -344,9 +323,7 @@ export default function LayoutsPage() {
<Card className="max-w-sm w-full">
<CardHeader>
<CardTitle>Centered Card</CardTitle>
<CardDescription>
Centered vertically and horizontally
</CardDescription>
<CardDescription>Centered vertically and horizontally</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
@@ -422,9 +399,7 @@ export default function LayoutsPage() {
<CardDescription>Most common pattern</CardDescription>
</CardHeader>
<CardContent>
<code className="text-xs">
grid-cols-1 md:grid-cols-2 lg:grid-cols-3
</code>
<code className="text-xs">grid-cols-1 md:grid-cols-2 lg:grid-cols-3</code>
<p className="mt-2 text-sm text-muted-foreground">
Mobile: 1 column
<br />
@@ -441,9 +416,7 @@ export default function LayoutsPage() {
<CardDescription>For smaller cards</CardDescription>
</CardHeader>
<CardContent>
<code className="text-xs">
grid-cols-1 md:grid-cols-2 lg:grid-cols-4
</code>
<code className="text-xs">grid-cols-1 md:grid-cols-2 lg:grid-cols-4</code>
<p className="mt-2 text-sm text-muted-foreground">
Mobile: 1 column
<br />
@@ -475,9 +448,7 @@ export default function LayoutsPage() {
<CardDescription>Mobile navigation</CardDescription>
</CardHeader>
<CardContent>
<code className="text-xs">
hidden lg:block
</code>
<code className="text-xs">hidden lg:block</code>
<p className="mt-2 text-sm text-muted-foreground">
Mobile: Hidden (use menu)
<br />

View File

@@ -6,23 +6,9 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import {
Palette,
Layout,
Ruler,
FileText,
BookOpen,
ArrowRight,
Sparkles,
} from 'lucide-react';
import { Palette, Layout, Ruler, FileText, BookOpen, ArrowRight, Sparkles } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
@@ -98,13 +84,11 @@ export default function DesignSystemHub() {
<div className="space-y-4 max-w-3xl">
<div className="flex items-center gap-2">
<Sparkles className="h-8 w-8 text-primary" />
<h1 className="text-4xl font-bold tracking-tight">
Design System Hub
</h1>
<h1 className="text-4xl font-bold tracking-tight">Design System Hub</h1>
</div>
<p className="text-lg text-muted-foreground">
Interactive demonstrations, live examples, and comprehensive documentation for
the FastNext design system. Built with shadcn/ui + Tailwind CSS 4.
Interactive demonstrations, live examples, and comprehensive documentation for the
FastNext design system. Built with shadcn/ui + Tailwind CSS 4.
</p>
</div>
</div>
@@ -116,9 +100,7 @@ export default function DesignSystemHub() {
{/* Demo Pages Grid */}
<section className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">
Interactive Demonstrations
</h2>
<h2 className="text-2xl font-semibold tracking-tight">Interactive Demonstrations</h2>
<p className="text-sm text-muted-foreground mt-2">
Explore live examples with copy-paste code snippets
</p>
@@ -143,9 +125,7 @@ export default function DesignSystemHub() {
New
</Badge>
)}
{page.status === 'enhanced' && (
<Badge variant="secondary">Enhanced</Badge>
)}
{page.status === 'enhanced' && <Badge variant="secondary">Enhanced</Badge>}
</div>
<CardTitle className="mt-4">{page.title}</CardTitle>
<CardDescription>{page.description}</CardDescription>
@@ -190,19 +170,13 @@ export default function DesignSystemHub() {
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{documentationLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="group"
>
<Link key={link.href} href={link.href} className="group">
<Card className="h-full transition-all hover:border-primary/50 hover:bg-accent/50">
<CardHeader className="space-y-1">
<CardTitle className="text-base group-hover:text-primary transition-colors">
{link.title}
</CardTitle>
<CardDescription className="text-xs">
{link.description}
</CardDescription>
<CardDescription className="text-xs">{link.description}</CardDescription>
</CardHeader>
</Card>
</Link>
@@ -214,9 +188,7 @@ export default function DesignSystemHub() {
{/* Key Features */}
<section className="space-y-6">
<h2 className="text-2xl font-semibold tracking-tight">
Key Features
</h2>
<h2 className="text-2xl font-semibold tracking-tight">Key Features</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<Card>

View File

@@ -10,13 +10,7 @@ import Link from 'next/link';
import { Ruler } from 'lucide-react';
import { DevBreadcrumbs } from '@/components/dev/DevBreadcrumbs';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
// Code-split heavy dev components
@@ -52,9 +46,9 @@ export default function SpacingPage() {
{/* Introduction */}
<div className="max-w-3xl space-y-4">
<p className="text-muted-foreground">
The Golden Rule: <strong>Parents control spacing, not children.</strong>{' '}
Use gap, space-y, and space-x utilities on the parent container. Avoid
margins on children except for exceptions.
The Golden Rule: <strong>Parents control spacing, not children.</strong> Use gap,
space-y, and space-x utilities on the parent container. Avoid margins on children
except for exceptions.
</p>
<div className="flex flex-wrap gap-2">
<Badge variant="outline">gap</Badge>
@@ -73,9 +67,7 @@ export default function SpacingPage() {
<Card>
<CardHeader>
<CardTitle>Common Spacing Values</CardTitle>
<CardDescription>
Use consistent spacing values from the scale
</CardDescription>
<CardDescription>Use consistent spacing values from the scale</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
@@ -95,10 +87,7 @@ export default function SpacingPage() {
<span className="text-sm text-muted-foreground">{item.rem}</span>
<span className="text-sm">{item.use}</span>
<div className="col-span-4">
<div
className="h-2 rounded bg-primary"
style={{ width: item.px }}
></div>
<div className="h-2 rounded bg-primary" style={{ width: item.px }}></div>
</div>
</div>
))}
@@ -158,10 +147,7 @@ export default function SpacingPage() {
<p className="text-sm font-medium mb-2">Grid (gap-6)</p>
<div className="grid grid-cols-3 gap-6">
{[1, 2, 3].map((i) => (
<div
key={i}
className="rounded-lg border bg-muted p-3 text-center text-sm"
>
<div key={i} className="rounded-lg border bg-muted p-3 text-center text-sm">
Card {i}
</div>
))}
@@ -207,12 +193,8 @@ export default function SpacingPage() {
<div className="rounded-lg border bg-muted p-3 text-sm">
First item (no margin)
</div>
<div className="rounded-lg border bg-muted p-3 text-sm">
Second item (mt-4)
</div>
<div className="rounded-lg border bg-muted p-3 text-sm">
Third item (mt-4)
</div>
<div className="rounded-lg border bg-muted p-3 text-sm">Second item (mt-4)</div>
<div className="rounded-lg border bg-muted p-3 text-sm">Third item (mt-4)</div>
</div>
</div>
@@ -245,7 +227,7 @@ export default function SpacingPage() {
title="Don't Let Children Control Spacing"
description="Parent should control spacing, not children"
before={{
caption: "Children control their own spacing with mt-4",
caption: 'Children control their own spacing with mt-4',
content: (
<div className="space-y-2 rounded-lg border p-4">
<div className="rounded bg-muted p-2 text-xs">
@@ -264,14 +246,12 @@ export default function SpacingPage() {
),
}}
after={{
caption: "Parent controls spacing with space-y-4",
caption: 'Parent controls spacing with space-y-4',
content: (
<div className="space-y-4 rounded-lg border p-4">
<div className="rounded bg-muted p-2 text-xs">
<div>Child 1</div>
<code className="text-[10px] text-green-600">
parent uses space-y-4
</code>
<code className="text-[10px] text-green-600">parent uses space-y-4</code>
</div>
<div className="rounded bg-muted p-2 text-xs">
<div>Child 2</div>
@@ -290,7 +270,7 @@ export default function SpacingPage() {
title="Use Gap, Not Margin for Buttons"
description="Button groups should use gap, not margins"
before={{
caption: "Margin on children - harder to maintain",
caption: 'Margin on children - harder to maintain',
content: (
<div className="flex rounded-lg border p-4">
<Button variant="outline" size="sm">
@@ -303,7 +283,7 @@ export default function SpacingPage() {
),
}}
after={{
caption: "Gap on parent - clean and flexible",
caption: 'Gap on parent - clean and flexible',
content: (
<div className="flex gap-4 rounded-lg border p-4">
<Button variant="outline" size="sm">

View File

@@ -20,23 +20,18 @@ export default function ForbiddenPage() {
<div className="container mx-auto px-6 py-16">
<div className="flex min-h-[60vh] flex-col items-center justify-center text-center">
<div className="mb-8 rounded-full bg-destructive/10 p-6">
<ShieldAlert
className="h-16 w-16 text-destructive"
aria-hidden="true"
/>
<ShieldAlert className="h-16 w-16 text-destructive" aria-hidden="true" />
</div>
<h1 className="mb-4 text-4xl font-bold tracking-tight">
403 - Access Forbidden
</h1>
<h1 className="mb-4 text-4xl font-bold tracking-tight">403 - Access Forbidden</h1>
<p className="mb-2 text-lg text-muted-foreground max-w-md">
You don&apos;t have permission to access this resource.
</p>
<p className="mb-8 text-sm text-muted-foreground max-w-md">
This page requires administrator privileges. If you believe you should
have access, please contact your system administrator.
This page requires administrator privileges. If you believe you should have access, please
contact your system administrator.
</p>
<div className="flex gap-4">

View File

@@ -1,4 +1,4 @@
@import "tailwindcss";
@import 'tailwindcss';
/**
* FastNext Template Design System
@@ -11,38 +11,38 @@
:root {
/* Colors */
--background: oklch(1.0000 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.3211 0 0);
--card: oklch(1.0000 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.3211 0 0);
--popover: oklch(1.0000 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.3211 0 0);
--primary: oklch(0.6231 0.1880 259.8145);
--primary-foreground: oklch(1.0000 0 0);
--secondary: oklch(0.9670 0.0029 264.5419);
--primary: oklch(0.6231 0.188 259.8145);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.967 0.0029 264.5419);
--secondary-foreground: oklch(0.4461 0.0263 256.8018);
--muted: oklch(0.9846 0.0017 247.8389);
--muted-foreground: oklch(0.5510 0.0234 264.3637);
--accent: oklch(0.9514 0.0250 236.8242);
--muted-foreground: oklch(0.551 0.0234 264.3637);
--accent: oklch(0.9514 0.025 236.8242);
--accent-foreground: oklch(0.3791 0.1378 265.5222);
--destructive: oklch(0.6368 0.2078 25.3313);
--destructive-foreground: oklch(1.0000 0 0);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.9276 0.0058 264.5313);
--input: oklch(0.9276 0.0058 264.5313);
--ring: oklch(0.6231 0.1880 259.8145);
--chart-1: oklch(0.6231 0.1880 259.8145);
--ring: oklch(0.6231 0.188 259.8145);
--chart-1: oklch(0.6231 0.188 259.8145);
--chart-2: oklch(0.5461 0.2152 262.8809);
--chart-3: oklch(0.4882 0.2172 264.3763);
--chart-4: oklch(0.4244 0.1809 265.6377);
--chart-5: oklch(0.3791 0.1378 265.5222);
--sidebar: oklch(0.9846 0.0017 247.8389);
--sidebar-foreground: oklch(0.3211 0 0);
--sidebar-primary: oklch(0.6231 0.1880 259.8145);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.9514 0.0250 236.8242);
--sidebar-primary: oklch(0.6231 0.188 259.8145);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.9514 0.025 236.8242);
--sidebar-accent-foreground: oklch(0.3791 0.1378 265.5222);
--sidebar-border: oklch(0.9276 0.0058 264.5313);
--sidebar-ring: oklch(0.6231 0.1880 259.8145);
--sidebar-ring: oklch(0.6231 0.188 259.8145);
/* Typography - Use Geist fonts from Next.js */
--font-sans: var(--font-geist-sans), system-ui, -apple-system, sans-serif;
@@ -61,11 +61,11 @@
--shadow-color: oklch(0 0 0);
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
/* Spacing */
@@ -81,8 +81,8 @@
--card-foreground: oklch(0.9219 0 0);
--popover: oklch(0.2686 0 0);
--popover-foreground: oklch(0.9219 0 0);
--primary: oklch(0.6231 0.1880 259.8145);
--primary-foreground: oklch(1.0000 0 0);
--primary: oklch(0.6231 0.188 259.8145);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.2686 0 0);
--secondary-foreground: oklch(0.9219 0 0);
--muted: oklch(0.2393 0 0);
@@ -90,23 +90,23 @@
--accent: oklch(0.3791 0.1378 265.5222);
--accent-foreground: oklch(0.8823 0.0571 254.1284);
--destructive: oklch(0.6368 0.2078 25.3313);
--destructive-foreground: oklch(1.0000 0 0);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.3715 0 0);
--input: oklch(0.3715 0 0);
--ring: oklch(0.6231 0.1880 259.8145);
--chart-1: oklch(0.7137 0.1434 254.6240);
--chart-2: oklch(0.6231 0.1880 259.8145);
--ring: oklch(0.6231 0.188 259.8145);
--chart-1: oklch(0.7137 0.1434 254.624);
--chart-2: oklch(0.6231 0.188 259.8145);
--chart-3: oklch(0.5461 0.2152 262.8809);
--chart-4: oklch(0.4882 0.2172 264.3763);
--chart-5: oklch(0.4244 0.1809 265.6377);
--sidebar: oklch(0.2046 0 0);
--sidebar-foreground: oklch(0.9219 0 0);
--sidebar-primary: oklch(0.6231 0.1880 259.8145);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-primary: oklch(0.6231 0.188 259.8145);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.3791 0.1378 265.5222);
--sidebar-accent-foreground: oklch(0.8823 0.0571 254.1284);
--sidebar-border: oklch(0.3715 0 0);
--sidebar-ring: oklch(0.6231 0.1880 259.8145);
--sidebar-ring: oklch(0.6231 0.188 259.8145);
}
/* Make CSS variables available to Tailwind utilities */
@@ -186,24 +186,24 @@ html.dark {
/* Cursor pointer for all clickable elements */
button,
[role="button"],
[type="button"],
[type="submit"],
[type="reset"],
[role='button'],
[type='button'],
[type='submit'],
[type='reset'],
a,
label[for],
select,
[tabindex]:not([tabindex="-1"]) {
[tabindex]:not([tabindex='-1']) {
cursor: pointer;
}
/* Exception: disabled elements should not have pointer cursor */
button:disabled,
[role="button"][aria-disabled="true"],
[type="button"]:disabled,
[type="submit"]:disabled,
[type="reset"]:disabled,
a[aria-disabled="true"],
[role='button'][aria-disabled='true'],
[type='button']:disabled,
[type='submit']:disabled,
[type='reset']:disabled,
a[aria-disabled='true'],
select:disabled {
cursor: not-allowed;
}

View File

@@ -1,27 +1,27 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Providers } from "./providers";
import { AuthProvider } from "@/lib/auth/AuthContext";
import { AuthInitializer } from "@/components/auth";
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
import { Providers } from './providers';
import { AuthProvider } from '@/lib/auth/AuthContext';
import { AuthInitializer } from '@/components/auth';
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
display: "swap", // Prevent font from blocking render
variable: '--font-geist-sans',
subsets: ['latin'],
display: 'swap', // Prevent font from blocking render
preload: true,
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
display: "swap", // Prevent font from blocking render
variable: '--font-geist-mono',
subsets: ['latin'],
display: 'swap', // Prevent font from blocking render
preload: false, // Only preload primary font
});
export const metadata: Metadata = {
title: "FastNext Template",
description: "FastAPI + Next.js Template",
title: 'FastNext Template',
description: 'FastAPI + Next.js Template',
};
export default function RootLayout({
@@ -57,9 +57,7 @@ export default function RootLayout({
}}
/>
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<AuthProvider>
<AuthInitializer />
<Providers>{children}</Providers>

View File

@@ -99,10 +99,7 @@ export default function Home() {
</footer>
{/* Shared Demo Credentials Modal */}
<DemoCredentialsModal
open={demoModalOpen}
onClose={() => setDemoModalOpen(false)}
/>
<DemoCredentialsModal open={demoModalOpen} onClose={() => setDemoModalOpen(false)} />
</div>
);
}

View File

@@ -8,8 +8,7 @@ import { ThemeProvider } from '@/components/theme';
// Set NEXT_PUBLIC_ENABLE_DEVTOOLS=true in .env.local to enable
/* istanbul ignore next - Dev-only devtools, not tested in production */
const ReactQueryDevtools =
process.env.NODE_ENV === 'development' &&
process.env.NEXT_PUBLIC_ENABLE_DEVTOOLS === 'true'
process.env.NODE_ENV === 'development' && process.env.NEXT_PUBLIC_ENABLE_DEVTOOLS === 'true'
? lazy(() =>
import('@tanstack/react-query-devtools').then((mod) => ({
default: mod.ReactQueryDevtools,

View File

@@ -64,9 +64,7 @@ export function AdminSidebar() {
<div className="flex h-full flex-col">
{/* Sidebar Header */}
<div className="flex h-16 items-center justify-between border-b px-4">
{!collapsed && (
<h2 className="text-lg font-semibold">Admin Panel</h2>
)}
{!collapsed && <h2 className="text-lg font-semibold">Admin Panel</h2>}
<button
onClick={() => setCollapsed(!collapsed)}
className="rounded-md p-2 hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
@@ -85,8 +83,7 @@ export function AdminSidebar() {
<nav className="flex-1 space-y-1 p-2">
{navItems.map((item) => {
const isActive =
pathname === item.href ||
(item.href !== '/admin' && pathname.startsWith(item.href));
pathname === item.href || (item.href !== '/admin' && pathname.startsWith(item.href));
const Icon = item.icon;
return (
@@ -97,9 +94,7 @@ export function AdminSidebar() {
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
'hover:bg-accent hover:text-accent-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
isActive
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground',
isActive ? 'bg-accent text-accent-foreground' : 'text-muted-foreground',
collapsed && 'justify-center'
)}
title={collapsed ? item.name : undefined}
@@ -123,9 +118,7 @@ export function AdminSidebar() {
<p className="text-sm font-medium truncate">
{user.first_name} {user.last_name}
</p>
<p className="text-xs text-muted-foreground truncate">
{user.email}
</p>
<p className="text-xs text-muted-foreground truncate">{user.email}</p>
</div>
</div>
</div>

View File

@@ -61,10 +61,7 @@ export function Breadcrumbs() {
return (
<li key={breadcrumb.href} className="flex items-center">
{index > 0 && (
<ChevronRight
className="mx-2 h-4 w-4 text-muted-foreground"
aria-hidden="true"
/>
<ChevronRight className="mx-2 h-4 w-4 text-muted-foreground" aria-hidden="true" />
)}
{isLast ? (
<span

View File

@@ -26,10 +26,7 @@ export function DashboardStats() {
}
return (
<div
className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"
data-testid="dashboard-stats"
>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4" data-testid="dashboard-stats">
<StatCard
title="Total Users"
value={stats?.totalUsers ?? 0}

View File

@@ -40,29 +40,20 @@ export function StatCard({
>
<div className="flex items-center justify-between">
<div className="space-y-1 flex-1">
<p
className="text-sm font-medium text-muted-foreground"
data-testid="stat-title"
>
<p className="text-sm font-medium text-muted-foreground" data-testid="stat-title">
{title}
</p>
<div className="flex items-baseline gap-2">
{loading ? (
<div className="h-8 w-24 bg-muted rounded" />
) : (
<p
className="text-3xl font-bold tracking-tight"
data-testid="stat-value"
>
<p className="text-3xl font-bold tracking-tight" data-testid="stat-value">
{value}
</p>
)}
</div>
{description && !loading && (
<p
className="text-xs text-muted-foreground"
data-testid="stat-description"
>
<p className="text-xs text-muted-foreground" data-testid="stat-description">
{description}
</p>
)}
@@ -74,22 +65,13 @@ export function StatCard({
)}
data-testid="stat-trend"
>
{trend.isPositive ? '↑' : '↓'} {Math.abs(trend.value)}%{' '}
{trend.label}
{trend.isPositive ? '↑' : '↓'} {Math.abs(trend.value)}% {trend.label}
</div>
)}
</div>
<div
className={cn(
'rounded-full p-3',
loading ? 'bg-muted' : 'bg-primary/10'
)}
>
<div className={cn('rounded-full p-3', loading ? 'bg-muted' : 'bg-primary/10')}>
<Icon
className={cn(
'h-6 w-6',
loading ? 'text-muted-foreground' : 'text-primary'
)}
className={cn('h-6 w-6', loading ? 'text-muted-foreground' : 'text-primary')}
aria-hidden="true"
/>
</div>

View File

@@ -47,11 +47,7 @@ interface AddMemberDialogProps {
organizationId: string;
}
export function AddMemberDialog({
open,
onOpenChange,
organizationId,
}: AddMemberDialogProps) {
export function AddMemberDialog({ open, onOpenChange, organizationId }: AddMemberDialogProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
// Fetch all users for the dropdown (simplified - in production, use search/autocomplete)
@@ -69,7 +65,12 @@ export function AddMemberDialog({
},
});
const { handleSubmit, formState: { errors }, setValue, watch } = form;
const {
handleSubmit,
formState: { errors },
setValue,
watch,
} = form;
const selectedRole = watch('role');
const selectedEmail = watch('userEmail');
@@ -139,7 +140,12 @@ export function AddMemberDialog({
{/* Role Select */}
<div className="space-y-2">
<Label htmlFor="role">Role *</Label>
<Select value={selectedRole} onValueChange={(value) => setValue('role', value as 'owner' | 'admin' | 'member' | 'guest')}>
<Select
value={selectedRole}
onValueChange={(value) =>
setValue('role', value as 'owner' | 'admin' | 'member' | 'guest')
}
>
<SelectTrigger id="role">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
@@ -150,9 +156,7 @@ export function AddMemberDialog({
<SelectItem value="guest">Guest</SelectItem>
</SelectContent>
</Select>
{errors.role && (
<p className="text-sm text-destructive">{errors.role.message}</p>
)}
{errors.role && <p className="text-sm text-destructive">{errors.role.message}</p>}
</div>
{/* Actions */}

View File

@@ -25,20 +25,14 @@ import {
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import {
useRemoveOrganizationMember,
type OrganizationMember,
} from '@/lib/api/hooks/useAdmin';
import { useRemoveOrganizationMember, type OrganizationMember } from '@/lib/api/hooks/useAdmin';
interface MemberActionMenuProps {
member: OrganizationMember;
organizationId: string;
}
export function MemberActionMenu({
member,
organizationId,
}: MemberActionMenuProps) {
export function MemberActionMenu({ member, organizationId }: MemberActionMenuProps) {
const [confirmRemove, setConfirmRemove] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(false);
@@ -59,9 +53,8 @@ export function MemberActionMenu({
}
};
const memberName = [member.first_name, member.last_name]
.filter(Boolean)
.join(' ') || member.email;
const memberName =
[member.first_name, member.last_name].filter(Boolean).join(' ') || member.email;
return (
<>
@@ -93,8 +86,8 @@ export function MemberActionMenu({
<AlertDialogHeader>
<AlertDialogTitle>Remove Member</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to remove {memberName} from this organization?
This action cannot be undone.
Are you sure you want to remove {memberName} from this organization? This action
cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>

View File

@@ -26,10 +26,7 @@ import {
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import {
useDeleteOrganization,
type Organization,
} from '@/lib/api/hooks/useAdmin';
import { useDeleteOrganization, type Organization } from '@/lib/api/hooks/useAdmin';
interface OrganizationActionMenuProps {
organization: Organization;
@@ -115,8 +112,8 @@ export function OrganizationActionMenu({
<AlertDialogHeader>
<AlertDialogTitle>Delete Organization</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete {organization.name}? This action cannot be undone
and will remove all associated data.
Are you sure you want to delete {organization.name}? This action cannot be undone and
will remove all associated data.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>

View File

@@ -112,7 +112,10 @@ export function OrganizationFormDialog({
toast.success(`${data.name} has been updated successfully.`);
} else {
// Generate slug from name (simple kebab-case conversion)
const slug = data.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
const slug = data.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
await createOrganization.mutateAsync({
name: data.name,
@@ -125,7 +128,9 @@ export function OrganizationFormDialog({
form.reset();
} catch (error) {
toast.error(
error instanceof Error ? error.message : `Failed to ${isEdit ? 'update' : 'create'} organization`
error instanceof Error
? error.message
: `Failed to ${isEdit ? 'update' : 'create'} organization`
);
}
};
@@ -137,9 +142,7 @@ export function OrganizationFormDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>
{isEdit ? 'Edit Organization' : 'Create Organization'}
</DialogTitle>
<DialogTitle>{isEdit ? 'Edit Organization' : 'Create Organization'}</DialogTitle>
<DialogDescription>
{isEdit
? 'Update the organization details below.'
@@ -189,15 +192,10 @@ export function OrganizationFormDialog({
<Checkbox
id="is_active"
checked={form.watch('is_active')}
onCheckedChange={(checked) =>
form.setValue('is_active', checked === true)
}
onCheckedChange={(checked) => form.setValue('is_active', checked === true)}
disabled={isLoading}
/>
<Label
htmlFor="is_active"
className="text-sm font-normal cursor-pointer"
>
<Label htmlFor="is_active" className="text-sm font-normal cursor-pointer">
Organization is active
</Label>
</div>

View File

@@ -93,9 +93,7 @@ export function OrganizationListTable({
<TableCell className="font-medium">{org.name}</TableCell>
<TableCell className="max-w-md truncate">
{org.description || (
<span className="text-muted-foreground italic">
No description
</span>
<span className="text-muted-foreground italic">No description</span>
)}
</TableCell>
<TableCell className="text-center">
@@ -112,9 +110,7 @@ export function OrganizationListTable({
{org.is_active ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>
{format(new Date(org.created_at), 'MMM d, yyyy')}
</TableCell>
<TableCell>{format(new Date(org.created_at), 'MMM d, yyyy')}</TableCell>
<TableCell>
<OrganizationActionMenu
organization={org}
@@ -135,11 +131,8 @@ export function OrganizationListTable({
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
Showing {(pagination.page - 1) * pagination.page_size + 1} to{' '}
{Math.min(
pagination.page * pagination.page_size,
pagination.total
)}{' '}
of {pagination.total} organizations
{Math.min(pagination.page * pagination.page_size, pagination.total)} of{' '}
{pagination.total} organizations
</div>
<div className="flex gap-2">
<Button
@@ -164,13 +157,9 @@ export function OrganizationListTable({
return (
<div key={page} className="flex items-center">
{showEllipsis && (
<span className="px-2 text-muted-foreground">...</span>
)}
{showEllipsis && <span className="px-2 text-muted-foreground">...</span>}
<Button
variant={
page === pagination.page ? 'default' : 'outline'
}
variant={page === pagination.page ? 'default' : 'outline'}
size="sm"
onClick={() => onPageChange(page)}
className="w-9"

View File

@@ -89,9 +89,7 @@ export function OrganizationManagementContent() {
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight">All Organizations</h2>
<p className="text-muted-foreground">
Manage organizations and their members
</p>
<p className="text-muted-foreground">Manage organizations and their members</p>
</div>
<Button onClick={handleCreateOrganization}>
<Plus className="mr-2 h-4 w-4" />

View File

@@ -85,9 +85,7 @@ export function OrganizationMembersContent({ organizationId }: OrganizationMembe
{/* Header with Add Member Button */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight">
{orgName} Members
</h2>
<h2 className="text-2xl font-bold tracking-tight">{orgName} Members</h2>
<p className="text-muted-foreground">
Manage members and their roles within the organization
</p>

View File

@@ -119,14 +119,9 @@ export function OrganizationMembersTable({
{formatRole(member.role)}
</Badge>
</TableCell>
<TableCell>{format(new Date(member.joined_at), 'MMM d, yyyy')}</TableCell>
<TableCell>
{format(new Date(member.joined_at), 'MMM d, yyyy')}
</TableCell>
<TableCell>
<MemberActionMenu
member={member}
organizationId={organizationId}
/>
<MemberActionMenu member={member} organizationId={organizationId} />
</TableCell>
</TableRow>
);
@@ -141,11 +136,8 @@ export function OrganizationMembersTable({
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
Showing {(pagination.page - 1) * pagination.page_size + 1} to{' '}
{Math.min(
pagination.page * pagination.page_size,
pagination.total
)}{' '}
of {pagination.total} members
{Math.min(pagination.page * pagination.page_size, pagination.total)} of{' '}
{pagination.total} members
</div>
<div className="flex gap-2">
<Button
@@ -170,13 +162,9 @@ export function OrganizationMembersTable({
return (
<div key={page} className="flex items-center">
{showEllipsis && (
<span className="px-2 text-muted-foreground">...</span>
)}
{showEllipsis && <span className="px-2 text-muted-foreground">...</span>}
<Button
variant={
page === pagination.page ? 'default' : 'outline'
}
variant={page === pagination.page ? 'default' : 'outline'}
size="sm"
onClick={() => onPageChange(page)}
className="w-9"

View File

@@ -62,9 +62,7 @@ export function BulkActionToolbar({
onClearSelection();
setPendingAction(null);
} catch (error) {
toast.error(
error instanceof Error ? error.message : `Failed to ${pendingAction} users`
);
toast.error(error instanceof Error ? error.message : `Failed to ${pendingAction} users`);
setPendingAction(null);
}
};
@@ -161,9 +159,7 @@ export function BulkActionToolbar({
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{getActionTitle()}</AlertDialogTitle>
<AlertDialogDescription>
{getActionDescription()}
</AlertDialogDescription>
<AlertDialogDescription>{getActionDescription()}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>

View File

@@ -49,9 +49,7 @@ export function UserActionMenu({ user, isCurrentUser, onEdit }: UserActionMenuPr
const deactivateUser = useDeactivateUser();
const deleteUser = useDeleteUser();
const fullName = user.last_name
? `${user.first_name} ${user.last_name}`
: user.first_name;
const fullName = user.last_name ? `${user.first_name} ${user.last_name}` : user.first_name;
// Handle activate action
const handleActivate = async () => {

View File

@@ -23,21 +23,14 @@ import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Alert } from '@/components/ui/alert';
import { toast } from 'sonner';
import {
useCreateUser,
useUpdateUser,
type User,
} from '@/lib/api/hooks/useAdmin';
import { useCreateUser, useUpdateUser, type User } from '@/lib/api/hooks/useAdmin';
// ============================================================================
// Validation Schema
// ============================================================================
const userFormSchema = z.object({
email: z
.string()
.min(1, 'Email is required')
.email('Please enter a valid email address'),
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
first_name: z
.string()
.min(1, 'First name is required')
@@ -66,12 +59,7 @@ interface UserFormDialogProps {
mode: 'create' | 'edit';
}
export function UserFormDialog({
open,
onOpenChange,
user,
mode,
}: UserFormDialogProps) {
export function UserFormDialog({ open, onOpenChange, user, mode }: UserFormDialogProps) {
const isEdit = mode === 'edit' && user;
const createUser = useCreateUser();
const updateUser = useUpdateUser();
@@ -130,7 +118,9 @@ export function UserFormDialog({
return;
}
if (!/[A-Z]/.test(data.password)) {
form.setError('password', { message: 'Password must contain at least one uppercase letter' });
form.setError('password', {
message: 'Password must contain at least one uppercase letter',
});
return;
}
}
@@ -147,7 +137,9 @@ export function UserFormDialog({
return;
}
if (!/[A-Z]/.test(data.password)) {
form.setError('password', { message: 'Password must contain at least one uppercase letter' });
form.setError('password', {
message: 'Password must contain at least one uppercase letter',
});
return;
}
}
@@ -305,10 +297,7 @@ export function UserFormDialog({
onCheckedChange={(checked) => setValue('is_active', checked as boolean)}
disabled={isSubmitting}
/>
<Label
htmlFor="is_active"
className="text-sm font-normal cursor-pointer"
>
<Label htmlFor="is_active" className="text-sm font-normal cursor-pointer">
Active (user can log in)
</Label>
</div>
@@ -320,10 +309,7 @@ export function UserFormDialog({
onCheckedChange={(checked) => setValue('is_superuser', checked as boolean)}
disabled={isSubmitting}
/>
<Label
htmlFor="is_superuser"
className="text-sm font-normal cursor-pointer"
>
<Label htmlFor="is_superuser" className="text-sm font-normal cursor-pointer">
Superuser (admin privileges)
</Label>
</div>
@@ -335,8 +321,8 @@ export function UserFormDialog({
{createUser.isError && createUser.error instanceof Error
? createUser.error.message
: updateUser.error instanceof Error
? updateUser.error.message
: 'An error occurred'}
? updateUser.error.message
: 'An error occurred'}
</Alert>
)}
@@ -355,8 +341,8 @@ export function UserFormDialog({
? 'Updating...'
: 'Creating...'
: isEdit
? 'Update User'
: 'Create User'}
? 'Update User'
: 'Create User'}
</Button>
</DialogFooter>
</form>

View File

@@ -74,8 +74,7 @@ export function UserListTable({
[onSearch]
);
const allSelected =
users.length > 0 && users.every((user) => selectedUsers.includes(user.id));
const allSelected = users.length > 0 && users.every((user) => selectedUsers.includes(user.id));
return (
<div className="space-y-4">
@@ -195,28 +194,18 @@ export function UserListTable({
</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell className="text-center">
<Badge
variant={user.is_active ? 'default' : 'secondary'}
>
<Badge variant={user.is_active ? 'default' : 'secondary'}>
{user.is_active ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell className="text-center">
{user.is_superuser ? (
<Check
className="h-4 w-4 mx-auto text-green-600"
aria-label="Yes"
/>
<Check className="h-4 w-4 mx-auto text-green-600" aria-label="Yes" />
) : (
<X
className="h-4 w-4 mx-auto text-muted-foreground"
aria-label="No"
/>
<X className="h-4 w-4 mx-auto text-muted-foreground" aria-label="No" />
)}
</TableCell>
<TableCell>
{format(new Date(user.created_at), 'MMM d, yyyy')}
</TableCell>
<TableCell>{format(new Date(user.created_at), 'MMM d, yyyy')}</TableCell>
<TableCell>
<UserActionMenu
user={user}
@@ -237,11 +226,8 @@ export function UserListTable({
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
Showing {(pagination.page - 1) * pagination.page_size + 1} to{' '}
{Math.min(
pagination.page * pagination.page_size,
pagination.total
)}{' '}
of {pagination.total} users
{Math.min(pagination.page * pagination.page_size, pagination.total)} of{' '}
{pagination.total} users
</div>
<div className="flex gap-2">
<Button
@@ -266,13 +252,9 @@ export function UserListTable({
return (
<div key={page} className="flex items-center">
{showEllipsis && (
<span className="px-2 text-muted-foreground">...</span>
)}
{showEllipsis && <span className="px-2 text-muted-foreground">...</span>}
<Button
variant={
page === pagination.page ? 'default' : 'outline'
}
variant={page === pagination.page ? 'default' : 'outline'}
size="sm"
onClick={() => onPageChange(page)}
className="w-9"

View File

@@ -28,7 +28,8 @@ export function UserManagementContent() {
// Convert filter strings to booleans for API
const isActiveFilter = filterActive === 'true' ? true : filterActive === 'false' ? false : null;
const isSuperuserFilter = filterSuperuser === 'true' ? true : filterSuperuser === 'false' ? false : null;
const isSuperuserFilter =
filterSuperuser === 'true' ? true : filterSuperuser === 'false' ? false : null;
// Local state
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
@@ -85,9 +86,7 @@ export function UserManagementContent() {
// istanbul ignore next - Event handlers fully tested in E2E (admin-users.spec.ts)
const handleSelectAll = (selected: boolean) => {
if (selected) {
const selectableUsers = users
.filter((u) => u.id !== currentUser?.id)
.map((u) => u.id);
const selectableUsers = users.filter((u) => u.id !== currentUser?.id).map((u) => u.id);
setSelectedUsers(selectableUsers);
} else {
setSelectedUsers([]);
@@ -141,9 +140,7 @@ export function UserManagementContent() {
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight">All Users</h2>
<p className="text-muted-foreground">
Manage user accounts and permissions
</p>
<p className="text-muted-foreground">Manage user accounts and permissions</p>
</div>
<Button onClick={handleCreateUser}>
<Plus className="mr-2 h-4 w-4" />

View File

@@ -83,9 +83,8 @@ export function AuthGuard({ children, requireAdmin = false, fallback }: AuthGuar
// If not loading and not authenticated, redirect to login
if (!isLoading && !isAuthenticated) {
// Preserve intended destination
const returnUrl = pathname !== config.routes.login
? `?returnUrl=${encodeURIComponent(pathname)}`
: '';
const returnUrl =
pathname !== config.routes.login ? `?returnUrl=${encodeURIComponent(pathname)}` : '';
router.push(`${config.routes.login}${returnUrl}`);
}

View File

@@ -21,9 +21,7 @@ export function AuthLayoutClient({ children }: AuthLayoutClientProps) {
{/* Auth card */}
<div className="w-full max-w-md">
<div className="rounded-lg border bg-card p-8 shadow-sm">
{children}
</div>
<div className="rounded-lg border bg-card p-8 shadow-sm">{children}</div>
</div>
</div>
);

View File

@@ -24,10 +24,7 @@ import config from '@/config/app.config';
// ============================================================================
const loginSchema = z.object({
email: z
.string()
.min(1, 'Email is required')
.email('Please enter a valid email address'),
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
password: z
.string()
.min(1, 'Password is required')
@@ -187,11 +184,7 @@ export function LoginForm({
</div>
{/* Submit Button */}
<Button
type="submit"
className="w-full"
disabled={isSubmitting}
>
<Button type="submit" className="w-full" disabled={isSubmitting}>
{isSubmitting ? 'Signing in...' : 'Sign in'}
</Button>

View File

@@ -57,8 +57,7 @@ function calculatePasswordStrength(password: string): {
const hasNumber = /[0-9]/.test(password);
const hasUppercase = /[A-Z]/.test(password);
const strength =
(hasMinLength ? 33 : 0) + (hasNumber ? 33 : 0) + (hasUppercase ? 34 : 0);
const strength = (hasMinLength ? 33 : 0) + (hasNumber ? 33 : 0) + (hasUppercase ? 34 : 0);
return { hasMinLength, hasNumber, hasUppercase, strength };
}
@@ -208,9 +207,7 @@ export function PasswordResetConfirmForm({
{...form.register('new_password')}
aria-invalid={!!form.formState.errors.new_password}
aria-describedby={
form.formState.errors.new_password
? 'new-password-error'
: 'password-requirements'
form.formState.errors.new_password ? 'new-password-error' : 'password-requirements'
}
aria-required="true"
/>
@@ -261,8 +258,7 @@ export function PasswordResetConfirmForm({
: 'text-muted-foreground'
}
>
{passwordStrength.hasUppercase ? '✓' : '○'} Contains an uppercase
letter
{passwordStrength.hasUppercase ? '✓' : '○'} Contains an uppercase letter
</li>
</ul>
</div>
@@ -283,9 +279,7 @@ export function PasswordResetConfirmForm({
{...form.register('confirm_password')}
aria-invalid={!!form.formState.errors.confirm_password}
aria-describedby={
form.formState.errors.confirm_password
? 'confirm-password-error'
: undefined
form.formState.errors.confirm_password ? 'confirm-password-error' : undefined
}
aria-required="true"
/>

View File

@@ -23,10 +23,7 @@ import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/erro
// ============================================================================
const resetRequestSchema = z.object({
email: z
.string()
.min(1, 'Email is required')
.email('Please enter a valid email address'),
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
});
type ResetRequestFormData = z.infer<typeof resetRequestSchema>;
@@ -169,11 +166,7 @@ export function PasswordResetRequestForm({
</div>
{/* Submit Button */}
<Button
type="submit"
className="w-full"
disabled={isSubmitting}
>
<Button type="submit" className="w-full" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send Reset Instructions'}
</Button>

View File

@@ -25,10 +25,7 @@ import config from '@/config/app.config';
const registerSchema = z
.object({
email: z
.string()
.min(1, 'Email is required')
.email('Please enter a valid email address'),
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
first_name: z
.string()
.min(1, 'First name is required')
@@ -45,9 +42,7 @@ const registerSchema = z
.min(8, 'Password must be at least 8 characters')
.regex(/[0-9]/, 'Password must contain at least one number')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter'),
confirmPassword: z
.string()
.min(1, 'Please confirm your password'),
confirmPassword: z.string().min(1, 'Please confirm your password'),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
@@ -88,11 +83,7 @@ interface RegisterFormProps {
* />
* ```
*/
export function RegisterForm({
onSuccess,
showLoginLink = true,
className,
}: RegisterFormProps) {
export function RegisterForm({ onSuccess, showLoginLink = true, className }: RegisterFormProps) {
const [serverError, setServerError] = useState<string | null>(null);
const registerMutation = useRegister();
@@ -242,7 +233,11 @@ export function RegisterForm({
disabled={isSubmitting}
{...form.register('password')}
aria-invalid={!!form.formState.errors.password}
aria-describedby={form.formState.errors.password ? 'password-error password-requirements' : 'password-requirements'}
aria-describedby={
form.formState.errors.password
? 'password-error password-requirements'
: 'password-requirements'
}
/>
{form.formState.errors.password && (
<p id="password-error" className="text-sm text-destructive">
@@ -253,13 +248,25 @@ export function RegisterForm({
{/* Password Strength Indicator */}
{password.length > 0 && !form.formState.errors.password && (
<div id="password-requirements" className="space-y-1 text-xs">
<p className={hasMinLength ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'}>
<p
className={
hasMinLength ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'
}
>
{hasMinLength ? '✓' : '○'} At least 8 characters
</p>
<p className={hasNumber ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'}>
<p
className={
hasNumber ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'
}
>
{hasNumber ? '✓' : '○'} Contains a number
</p>
<p className={hasUppercase ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'}>
<p
className={
hasUppercase ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'
}
>
{hasUppercase ? '✓' : '○'} Contains an uppercase letter
</p>
</div>
@@ -279,7 +286,9 @@ export function RegisterForm({
disabled={isSubmitting}
{...form.register('confirmPassword')}
aria-invalid={!!form.formState.errors.confirmPassword}
aria-describedby={form.formState.errors.confirmPassword ? 'confirmPassword-error' : undefined}
aria-describedby={
form.formState.errors.confirmPassword ? 'confirmPassword-error' : undefined
}
/>
{form.formState.errors.confirmPassword && (
<p id="confirmPassword-error" className="text-sm text-destructive">
@@ -289,11 +298,7 @@ export function RegisterForm({
</div>
{/* Submit Button */}
<Button
type="submit"
className="w-full"
disabled={isSubmitting}
>
<Button type="submit" className="w-full" disabled={isSubmitting}>
{isSubmitting ? 'Creating account...' : 'Create account'}
</Button>

View File

@@ -5,7 +5,16 @@
'use client';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from 'recharts';
import { ChartCard } from './ChartCard';
import { CHART_PALETTES } from '@/lib/chart-colors';

View File

@@ -5,7 +5,16 @@
'use client';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from 'recharts';
import { ChartCard } from './ChartCard';
import { format, subDays } from 'date-fns';
import { CHART_PALETTES } from '@/lib/chart-colors';

View File

@@ -5,7 +5,16 @@
'use client';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from 'recharts';
import { ChartCard } from './ChartCard';
import { format, subDays } from 'date-fns';
import { CHART_PALETTES } from '@/lib/chart-colors';

View File

@@ -61,19 +61,12 @@ export function BeforeAfter({
{(title || description) && (
<div className="space-y-2">
{title && <h3 className="text-xl font-semibold">{title}</h3>}
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</div>
)}
{/* Comparison Grid */}
<div
className={cn(
'grid gap-4',
vertical ? 'grid-cols-1' : 'grid-cols-1 lg:grid-cols-2'
)}
>
<div className={cn('grid gap-4', vertical ? 'grid-cols-1' : 'grid-cols-1 lg:grid-cols-2')}>
{/* Before (Anti-pattern) */}
<Card className="border-destructive/50">
<CardHeader className="space-y-2 pb-4">
@@ -94,9 +87,7 @@ export function BeforeAfter({
</div>
{/* Caption */}
{before.caption && (
<p className="text-xs text-muted-foreground italic">
{before.caption}
</p>
<p className="text-xs text-muted-foreground italic">{before.caption}</p>
)}
</CardContent>
</Card>
@@ -124,9 +115,7 @@ export function BeforeAfter({
</div>
{/* Caption */}
{after.caption && (
<p className="text-xs text-muted-foreground italic">
{after.caption}
</p>
<p className="text-xs text-muted-foreground italic">{after.caption}</p>
)}
</CardContent>
</Card>

View File

@@ -61,12 +61,8 @@ export function CodeSnippet({
{(title || language) && (
<div className="flex items-center justify-between rounded-t-lg border border-b-0 bg-muted/50 px-4 py-2">
<div className="flex items-center gap-2">
{title && (
<span className="text-sm font-medium text-foreground">{title}</span>
)}
{language && (
<span className="text-xs text-muted-foreground">({language})</span>
)}
{title && <span className="text-sm font-medium text-foreground">{title}</span>}
{language && <span className="text-xs text-muted-foreground">({language})</span>}
</div>
<Button
variant="ghost"
@@ -139,8 +135,7 @@ export function CodeSnippet({
key={idx}
className={cn(
'leading-6',
highlightLines.includes(idx + 1) &&
'bg-accent/20 -mx-4 px-4'
highlightLines.includes(idx + 1) && 'bg-accent/20 -mx-4 px-4'
)}
>
{line || ' '}

View File

@@ -9,11 +9,7 @@
'use client';
import { useState } from 'react';
import {
Mail, User,
Settings, LogOut, Shield, AlertCircle, Info,
Trash2
} from 'lucide-react';
import { Mail, User, Settings, LogOut, Shield, AlertCircle, Info, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Card,
@@ -281,7 +277,11 @@ export function ComponentShowcase() {
</div>
<div className="flex items-center space-x-2">
<Checkbox id="terms" checked={checked} onCheckedChange={(value) => setChecked(value as boolean)} />
<Checkbox
id="terms"
checked={checked}
onCheckedChange={(value) => setChecked(value as boolean)}
/>
<Label htmlFor="terms" className="text-sm font-normal cursor-pointer">
Accept terms and conditions
</Label>
@@ -357,11 +357,7 @@ export function ComponentShowcase() {
</ExampleSection>
{/* Badges */}
<ExampleSection
id="badges"
title="Badges"
description="Status indicators and labels"
>
<ExampleSection id="badges" title="Badges" description="Status indicators and labels">
<Example
title="Badge Variants"
code={`<Badge>Default</Badge>
@@ -411,11 +407,7 @@ export function ComponentShowcase() {
</ExampleSection>
{/* Alerts */}
<ExampleSection
id="alerts"
title="Alerts"
description="Contextual feedback messages"
>
<ExampleSection id="alerts" title="Alerts" description="Contextual feedback messages">
<div className="space-y-4">
<Example
title="Alert Variants"
@@ -439,17 +431,13 @@ export function ComponentShowcase() {
<Alert>
<Info className="h-4 w-4" />
<AlertTitle>Information</AlertTitle>
<AlertDescription>
This is an informational alert message.
</AlertDescription>
<AlertDescription>This is an informational alert message.</AlertDescription>
</Alert>
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Something went wrong. Please try again.
</AlertDescription>
<AlertDescription>Something went wrong. Please try again.</AlertDescription>
</Alert>
</div>
</Example>
@@ -545,8 +533,8 @@ export function ComponentShowcase() {
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
This action cannot be undone. This will permanently delete your account and
remove your data from our servers.
</DialogDescription>
</DialogHeader>
<DialogFooter>
@@ -594,9 +582,7 @@ export function ComponentShowcase() {
</div>
</TabsContent>
<TabsContent value="password" className="space-y-4 mt-4">
<p className="text-sm text-muted-foreground">
Change your password here.
</p>
<p className="text-sm text-muted-foreground">Change your password here.</p>
<div className="space-y-2">
<Label htmlFor="current">Current Password</Label>
<Input id="current" type="password" />
@@ -607,11 +593,7 @@ export function ComponentShowcase() {
</ExampleSection>
{/* Table */}
<ExampleSection
id="table"
title="Table"
description="Data tables with headers and cells"
>
<ExampleSection id="table" title="Table" description="Data tables with headers and cells">
<Example
title="Table Example"
code={`<Table>

View File

@@ -24,10 +24,7 @@ interface DevBreadcrumbsProps {
export function DevBreadcrumbs({ items, className }: DevBreadcrumbsProps) {
return (
<nav
className={cn('bg-muted/30 border-b', className)}
aria-label="Breadcrumb"
>
<nav className={cn('bg-muted/30 border-b', className)} aria-label="Breadcrumb">
<div className="container mx-auto px-4 py-3">
<ol className="flex items-center gap-2 text-sm">
{/* Home link */}

View File

@@ -48,7 +48,6 @@ export function Example({
centered = false,
tags,
}: ExampleProps) {
// Compact variant - no card wrapper
if (variant === 'compact') {
return (
@@ -68,9 +67,7 @@ export function Example({
</div>
)}
</div>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</div>
)}
@@ -178,9 +175,7 @@ export function ExampleGrid({
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
}[cols];
return (
<div className={cn('grid gap-6', colsClass, className)}>{children}</div>
);
return <div className={cn('grid gap-6', colsClass, className)}>{children}</div>;
}
/**
@@ -208,9 +203,7 @@ export function ExampleSection({
<section id={id} className={cn('space-y-6', className)}>
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">{title}</h2>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</div>
{children}
</section>

View File

@@ -74,7 +74,9 @@ function extractTextFromChildren(children: React.ReactNode): string {
}
if (children && typeof children === 'object' && 'props' in children) {
return extractTextFromChildren((children as { props: { children: React.ReactNode } }).props.children);
return extractTextFromChildren(
(children as { props: { children: React.ReactNode } }).props.children
);
}
return '';

View File

@@ -101,7 +101,10 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
</ul>
),
ol: ({ children, ...props }) => (
<ol className="my-6 ml-6 list-decimal space-y-3 marker:text-primary/60 marker:font-semibold" {...props}>
<ol
className="my-6 ml-6 list-decimal space-y-3 marker:text-primary/60 marker:font-semibold"
{...props}
>
{children}
</ol>
),
@@ -112,7 +115,12 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
),
// Code blocks - enhanced with copy button and better styling
code: ({ inline, className, children, ...props }: {
code: ({
inline,
className,
children,
...props
}: {
inline?: boolean;
className?: string;
children?: React.ReactNode;
@@ -128,22 +136,12 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
);
}
return (
<code
className={cn(
'block font-mono text-sm leading-relaxed',
className
)}
{...props}
>
<code className={cn('block font-mono text-sm leading-relaxed', className)} {...props}>
{children}
</code>
);
},
pre: ({ children, ...props }) => (
<CodeBlock {...props}>
{children}
</CodeBlock>
),
pre: ({ children, ...props }) => <CodeBlock {...props}>{children}</CodeBlock>,
// Blockquotes - enhanced callout styling
blockquote: ({ children, ...props }) => (
@@ -158,10 +156,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
// Tables - improved styling with better borders and hover states
table: ({ children, ...props }) => (
<div className="my-8 w-full overflow-x-auto rounded-lg border">
<table
className="w-full border-collapse text-sm"
{...props}
>
<table className="w-full border-collapse text-sm" {...props}>
{children}
</table>
</div>
@@ -172,7 +167,9 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
</thead>
),
tbody: ({ children, ...props }) => (
<tbody className="divide-y divide-border" {...props}>{children}</tbody>
<tbody className="divide-y divide-border" {...props}>
{children}
</tbody>
),
tr: ({ children, ...props }) => (
<tr className="transition-colors hover:bg-muted/40" {...props}>
@@ -197,9 +194,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
),
// Horizontal rule - more prominent
hr: ({ ...props }) => (
<hr className="my-12 border-t-2 border-border/50" {...props} />
),
hr: ({ ...props }) => <hr className="my-12 border-t-2 border-border/50" {...props} />,
// Images - optimized with Next.js Image component
img: ({ src, alt }) => {

View File

@@ -60,7 +60,7 @@ export function FormField({
}: FormFieldProps) {
// Extract name from inputProps (from register()) or use explicit name
// register() adds a name property that may not be in the type
const registerName = ('name' in inputProps) ? (inputProps as { name: string }).name : undefined;
const registerName = 'name' in inputProps ? (inputProps as { name: string }).name : undefined;
const name = explicitName || registerName;
if (!name) {
@@ -84,12 +84,7 @@ export function FormField({
{description}
</p>
)}
<Input
id={name}
aria-invalid={!!error}
aria-describedby={ariaDescribedBy}
{...inputProps}
/>
<Input id={name} aria-invalid={!!error} aria-describedby={ariaDescribedBy} {...inputProps} />
{error && (
<p id={errorId} className="text-sm text-destructive" role="alert">
{error.message}

View File

@@ -94,7 +94,10 @@ export function AnimatedTerminal() {
</div>
{/* Terminal Content */}
<div className="bg-slate-950 p-6 font-mono text-sm overflow-x-auto" style={{ minHeight: '400px' }}>
<div
className="bg-slate-950 p-6 font-mono text-sm overflow-x-auto"
style={{ minHeight: '400px' }}
>
<div className="space-y-2">
{displayedLines.map((line, index) => (
<motion.div
@@ -106,15 +109,18 @@ export function AnimatedTerminal() {
line.isSuccess
? 'text-green-400'
: line.text.startsWith('#')
? 'text-slate-500'
: line.text.startsWith('$')
? 'text-blue-400'
: 'text-slate-300'
? 'text-slate-500'
: line.text.startsWith('$')
? 'text-blue-400'
: 'text-slate-300'
}`}
>
{line.text || '\u00A0'}
{index === displayedLines.length - 1 && isAnimating && !line.isSuccess && (
<span className="inline-block w-2 h-4 ml-1 bg-slate-400 animate-pulse" aria-hidden="true" />
<span
className="inline-block w-2 h-4 ml-1 bg-slate-400 animate-pulse"
aria-hidden="true"
/>
)}
</motion.div>
))}

View File

@@ -15,7 +15,6 @@ interface CTASectionProps {
}
export function CTASection({ onOpenDemoModal }: CTASectionProps) {
return (
<section className="relative overflow-hidden bg-gradient-to-br from-primary/10 via-background to-background">
{/* Background Pattern */}
@@ -48,11 +47,7 @@ export function CTASection({ onOpenDemoModal }: CTASectionProps) {
{/* CTAs */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4">
<Button
asChild
size="lg"
className="gap-2 text-base group"
>
<Button asChild size="lg" className="gap-2 text-base group">
<a
href="https://github.com/your-org/fast-next-template"
target="_blank"
@@ -72,22 +67,23 @@ export function CTASection({ onOpenDemoModal }: CTASectionProps) {
variant="outline"
className="gap-2 text-base group"
>
<Play className="h-5 w-5 group-hover:scale-110 transition-transform" aria-hidden="true" />
<Play
className="h-5 w-5 group-hover:scale-110 transition-transform"
aria-hidden="true"
/>
Try Live Demo
</Button>
<Button
asChild
size="lg"
variant="ghost"
className="gap-2 text-base group"
>
<Button asChild size="lg" variant="ghost" className="gap-2 text-base group">
<a
href="https://github.com/your-org/fast-next-template#documentation"
target="_blank"
rel="noopener noreferrer"
>
Read Documentation
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" aria-hidden="true" />
<ArrowRight
className="h-4 w-4 group-hover:translate-x-1 transition-transform"
aria-hidden="true"
/>
</a>
</Button>
</div>
@@ -104,8 +100,8 @@ export function CTASection({ onOpenDemoModal }: CTASectionProps) {
Need help getting started? Check out the{' '}
<Link href="/dev" className="text-primary hover:underline">
component showcase
</Link>
{' '}or explore the{' '}
</Link>{' '}
or explore the{' '}
<Link href="/admin" className="text-primary hover:underline">
admin dashboard demo
</Link>

View File

@@ -26,9 +26,7 @@ export function ContextSection() {
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl md:text-4xl font-bold">
What You Get Out of the Box
</h2>
<h2 className="text-3xl md:text-4xl font-bold">What You Get Out of the Box</h2>
<p className="text-lg text-muted-foreground leading-relaxed">
This isn&apos;t a boilerplate generator or a paid SaaS template. It&apos;s a complete,
production-ready codebase you can clone and customize. Everything you need to build

View File

@@ -51,7 +51,8 @@ export function DemoCredentialsModal({ open, onClose }: DemoCredentialsModalProp
<DialogHeader>
<DialogTitle>Try the Live Demo</DialogTitle>
<DialogDescription>
Use these credentials to explore the template&apos;s features. Both accounts are pre-configured with sample data.
Use these credentials to explore the template&apos;s features. Both accounts are
pre-configured with sample data.
</DialogDescription>
</DialogHeader>

View File

@@ -54,7 +54,8 @@ export function DemoSection() {
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">See It In Action</h2>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Explore the template&apos;s capabilities with live demos. Login with demo credentials to test features.
Explore the template&apos;s capabilities with live demos. Login with demo credentials to
test features.
</p>
</motion.div>
@@ -75,9 +76,7 @@ export function DemoSection() {
</div>
<h3 className="text-xl font-semibold mb-2">{demo.title}</h3>
<p className="text-muted-foreground mb-4 leading-relaxed flex-1">
{demo.description}
</p>
<p className="text-muted-foreground mb-4 leading-relaxed flex-1">{demo.description}</p>
{demo.credentials && (
<div className="mb-4 p-3 rounded-md bg-muted font-mono text-xs border">

View File

@@ -95,7 +95,8 @@ export function FeatureGrid() {
Comprehensive Features, No Assembly Required
</h2>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Everything you need to build production-grade web applications. Clone, customize, and ship.
Everything you need to build production-grade web applications. Clone, customize, and
ship.
</p>
</motion.div>

View File

@@ -31,8 +31,7 @@ export function Header({ onOpenDemoModal }: HeaderProps) {
<Link href="/" className="font-bold text-xl hover:opacity-80 transition-opacity">
<span className="bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
FastNext
</span>
{' '}
</span>{' '}
<span className="text-foreground">Template</span>
</Link>
@@ -64,18 +63,10 @@ export function Header({ onOpenDemoModal }: HeaderProps) {
</a>
{/* CTAs */}
<Button
onClick={onOpenDemoModal}
variant="default"
size="sm"
>
<Button onClick={onOpenDemoModal} variant="default" size="sm">
Try Demo
</Button>
<Button
asChild
variant="outline"
size="sm"
>
<Button asChild variant="outline" size="sm">
<Link href="/login">Login</Link>
</Button>
</nav>
@@ -131,11 +122,7 @@ export function Header({ onOpenDemoModal }: HeaderProps) {
>
Try Demo
</Button>
<Button
asChild
variant="outline"
className="w-full"
>
<Button asChild variant="outline" className="w-full">
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
Login
</Link>

View File

@@ -15,11 +15,13 @@ interface HeroSectionProps {
}
export function HeroSection({ onOpenDemoModal }: HeroSectionProps) {
return (
<section className="relative overflow-hidden">
{/* Gradient Background */}
<div className="absolute inset-0 -z-10 bg-gradient-to-br from-primary/5 via-background to-background" aria-hidden="true" />
<div
className="absolute inset-0 -z-10 bg-gradient-to-br from-primary/5 via-background to-background"
aria-hidden="true"
/>
<div
className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_30%_20%,rgba(var(--primary-rgb,120,119,198),0.1),transparent_50%)]"
aria-hidden="true"
@@ -38,7 +40,10 @@ export function HeroSection({ onOpenDemoModal }: HeroSectionProps) {
transition={{ duration: 0.5 }}
>
<div className="inline-flex items-center gap-2 rounded-full border bg-background/50 px-4 py-1.5 text-sm backdrop-blur">
<span className="inline-block h-2 w-2 rounded-full bg-green-500 animate-pulse" aria-hidden="true" />
<span
className="inline-block h-2 w-2 rounded-full bg-green-500 animate-pulse"
aria-hidden="true"
/>
<span className="font-medium">MIT Licensed</span>
<span className="text-muted-foreground"></span>
<span className="font-medium">97% Test Coverage</span>
@@ -79,20 +84,14 @@ export function HeroSection({ onOpenDemoModal }: HeroSectionProps) {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<Button
size="lg"
onClick={onOpenDemoModal}
className="gap-2 text-base group"
>
<Play className="h-5 w-5 group-hover:scale-110 transition-transform" aria-hidden="true" />
<Button size="lg" onClick={onOpenDemoModal} className="gap-2 text-base group">
<Play
className="h-5 w-5 group-hover:scale-110 transition-transform"
aria-hidden="true"
/>
Try Live Demo
</Button>
<Button
asChild
size="lg"
variant="outline"
className="gap-2 text-base group"
>
<Button asChild size="lg" variant="outline" className="gap-2 text-base group">
<a
href="https://github.com/your-org/fast-next-template"
target="_blank"
@@ -100,18 +99,19 @@ export function HeroSection({ onOpenDemoModal }: HeroSectionProps) {
>
<Github className="h-5 w-5" aria-hidden="true" />
View on GitHub
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" aria-hidden="true" />
<ArrowRight
className="h-4 w-4 group-hover:translate-x-1 transition-transform"
aria-hidden="true"
/>
</a>
</Button>
<Button
asChild
size="lg"
variant="ghost"
className="gap-2 text-base group"
>
<Button asChild size="lg" variant="ghost" className="gap-2 text-base group">
<Link href="/dev">
Explore Components
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" aria-hidden="true" />
<ArrowRight
className="h-4 w-4 group-hover:translate-x-1 transition-transform"
aria-hidden="true"
/>
</Link>
</Button>
</motion.div>

View File

@@ -33,22 +33,18 @@ export function PhilosophySection() {
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl md:text-4xl font-bold mb-6">
Why This Template Exists
</h2>
<h2 className="text-3xl md:text-4xl font-bold mb-6">Why This Template Exists</h2>
<div className="space-y-4 text-lg text-muted-foreground leading-relaxed">
<p>
We built this template after rebuilding the same authentication, authorization, and
admin infrastructure for the fifth time. Instead of yet another tutorial or boilerplate
generator, we created a complete, tested, documented codebase that you can clone and
customize.
admin infrastructure for the fifth time. Instead of yet another tutorial or
boilerplate generator, we created a complete, tested, documented codebase that you can
clone and customize.
</p>
<p className="text-foreground font-semibold text-xl">
No vendor lock-in. No subscriptions. No license restrictions.
</p>
<p>
Just clean, modern code with patterns that scale. MIT licensed forever.
</p>
<p>Just clean, modern code with patterns that scale. MIT licensed forever.</p>
</div>
</motion.div>
@@ -90,7 +86,10 @@ export function PhilosophySection() {
<ul className="space-y-3">
{willFind.map((item) => (
<li key={item} className="flex items-start gap-3">
<Check className="h-5 w-5 text-green-600 flex-shrink-0 mt-0.5" aria-hidden="true" />
<Check
className="h-5 w-5 text-green-600 flex-shrink-0 mt-0.5"
aria-hidden="true"
/>
<span className="text-muted-foreground">{item}</span>
</li>
))}

View File

@@ -58,12 +58,7 @@ export function QuickStartCode() {
<span className="inline-block h-2 w-2 rounded-full bg-green-500" aria-hidden="true" />
<span>bash</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={copyToClipboard}
className="gap-2"
>
<Button variant="ghost" size="sm" onClick={copyToClipboard} className="gap-2">
{copied ? (
<>
<Check className="h-4 w-4 text-green-600" aria-hidden="true" />

View File

@@ -95,7 +95,8 @@ export function StatsSection() {
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">Built with Quality in Mind</h2>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Not just another template. Comprehensive testing, documentation, and production-ready patterns.
Not just another template. Comprehensive testing, documentation, and production-ready
patterns.
</p>
</motion.div>

View File

@@ -70,7 +70,8 @@ export function TechStackSection() {
Modern, Type-Safe, Production-Grade Stack
</h2>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Built with the best tools for full-stack development. Async architecture, type safety, and developer experience.
Built with the best tools for full-stack development. Async architecture, type safety, and
developer experience.
</p>
</motion.div>
@@ -87,7 +88,9 @@ export function TechStackSection() {
>
<div className="h-full flex flex-col items-center justify-center rounded-lg border bg-card p-6 text-center hover:shadow-lg transition-all">
{/* Tech Badge */}
<div className={`inline-block rounded-full bg-gradient-to-r ${tech.color} px-4 py-2 text-white font-semibold text-sm mb-2`}>
<div
className={`inline-block rounded-full bg-gradient-to-r ${tech.color} px-4 py-2 text-white font-semibold text-sm mb-2`}
>
{tech.name}
</div>

View File

@@ -80,9 +80,7 @@ export function Header() {
{/* Logo */}
<div className="flex items-center space-x-8">
<Link href="/" className="flex items-center space-x-2">
<span className="text-xl font-bold text-foreground">
FastNext
</span>
<span className="text-xl font-bold text-foreground">FastNext</span>
</Link>
{/* Navigation Links */}
@@ -90,11 +88,7 @@ export function Header() {
<NavLink href="/" exact>
Home
</NavLink>
{user?.is_superuser && (
<NavLink href="/admin">
Admin
</NavLink>
)}
{user?.is_superuser && <NavLink href="/admin">Admin</NavLink>}
</nav>
</div>
@@ -117,9 +111,7 @@ export function Header() {
<p className="text-sm font-medium">
{user?.first_name} {user?.last_name}
</p>
<p className="text-xs text-muted-foreground">
{user?.email}
</p>
<p className="text-xs text-muted-foreground">{user?.email}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />

View File

@@ -22,25 +22,23 @@ import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/erro
// Validation Schema
// ============================================================================
const passwordChangeSchema = z.object({
current_password: z
.string()
.min(1, 'Current password is required'),
new_password: z
.string()
.min(1, 'New password is required')
.min(8, 'Password must be at least 8 characters')
.regex(/[0-9]/, 'Password must contain at least one number')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
confirm_password: z
.string()
.min(1, 'Please confirm your new password'),
}).refine((data) => data.new_password === data.confirm_password, {
message: 'Passwords do not match',
path: ['confirm_password'],
});
const passwordChangeSchema = z
.object({
current_password: z.string().min(1, 'Current password is required'),
new_password: z
.string()
.min(1, 'New password is required')
.min(8, 'Password must be at least 8 characters')
.regex(/[0-9]/, 'Password must contain at least one number')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
confirm_password: z.string().min(1, 'Please confirm your new password'),
})
.refine((data) => data.new_password === data.confirm_password, {
message: 'Passwords do not match',
path: ['confirm_password'],
});
type PasswordChangeFormData = z.infer<typeof passwordChangeSchema>;
@@ -73,10 +71,7 @@ interface PasswordChangeFormProps {
* <PasswordChangeForm onSuccess={() => console.log('Password changed')} />
* ```
*/
export function PasswordChangeForm({
onSuccess,
className,
}: PasswordChangeFormProps) {
export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormProps) {
const [serverError, setServerError] = useState<string | null>(null);
const passwordChangeMutation = usePasswordChange((message) => {
toast.success(message);
@@ -191,19 +186,12 @@ export function PasswordChangeForm({
{/* Submit Button */}
<div className="flex items-center gap-4">
<Button
type="submit"
disabled={isSubmitting || !isDirty}
>
<Button type="submit" disabled={isSubmitting || !isDirty}>
{isSubmitting ? 'Changing Password...' : 'Change Password'}
</Button>
{/* istanbul ignore next - Cancel button requires isDirty state, tested in E2E */}
{isDirty && !isSubmitting && (
<Button
type="button"
variant="outline"
onClick={() => form.reset()}
>
<Button type="button" variant="outline" onClick={() => form.reset()}>
Cancel
</Button>
)}

View File

@@ -34,9 +34,7 @@ const profileSchema = z.object({
.max(50, 'Last name must not exceed 50 characters')
.optional()
.or(z.literal('')),
email: z
.string()
.email('Invalid email address'),
email: z.string().email('Invalid email address'),
});
type ProfileFormData = z.infer<typeof profileSchema>;
@@ -68,10 +66,7 @@ interface ProfileSettingsFormProps {
* <ProfileSettingsForm onSuccess={() => console.log('Profile updated')} />
* ```
*/
export function ProfileSettingsForm({
onSuccess,
className,
}: ProfileSettingsFormProps) {
export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFormProps) {
const [serverError, setServerError] = useState<string | null>(null);
const currentUser = useCurrentUser();
const updateProfileMutation = useUpdateProfile((message) => {
@@ -201,19 +196,12 @@ export function ProfileSettingsForm({
{/* Submit Button */}
<div className="flex items-center gap-4">
<Button
type="submit"
disabled={isSubmitting || !isDirty}
>
<Button type="submit" disabled={isSubmitting || !isDirty}>
{isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
{/* istanbul ignore next - Reset button requires isDirty state, tested in E2E */}
{isDirty && !isSubmitting && (
<Button
type="button"
variant="outline"
onClick={() => form.reset()}
>
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
)}

View File

@@ -61,7 +61,9 @@ export function SessionCard({ session, onRevoke, isRevoking = false }: SessionCa
const DeviceIcon = Monitor;
// Format location string
const location = [session.location_city, session.location_country].filter(Boolean).join(', ') || 'Unknown location';
const location =
[session.location_city, session.location_country].filter(Boolean).join(', ') ||
'Unknown location';
// Format device string
const deviceInfo = session.device_name || 'Unknown device';
@@ -145,8 +147,8 @@ export function SessionCard({ session, onRevoke, isRevoking = false }: SessionCa
<DialogHeader>
<DialogTitle>Revoke Session?</DialogTitle>
<DialogDescription>
This will sign out this device and you&apos;ll need to sign in again to access your account
from it. This action cannot be undone.
This will sign out this device and you&apos;ll need to sign in again to access your
account from it. This action cannot be undone.
</DialogDescription>
</DialogHeader>
<div className="py-4">

View File

@@ -21,7 +21,11 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { SessionCard } from './SessionCard';
import { useListSessions, useRevokeSession, useRevokeAllOtherSessions } from '@/lib/api/hooks/useSession';
import {
useListSessions,
useRevokeSession,
useRevokeAllOtherSessions,
} from '@/lib/api/hooks/useSession';
import { useState } from 'react';
// ============================================================================
@@ -195,8 +199,8 @@ export function SessionsManager({ className }: SessionsManagerProps) {
{/* Security Tip */}
<div className="pt-4 border-t">
<p className="text-sm text-muted-foreground">
<strong className="text-foreground">Security tip:</strong> If you see a session you don&apos;t
recognize, revoke it immediately and change your password.
<strong className="text-foreground">Security tip:</strong> If you see a session you
don&apos;t recognize, revoke it immediately and change your password.
</p>
</div>
</CardContent>
@@ -208,8 +212,8 @@ export function SessionsManager({ className }: SessionsManagerProps) {
<DialogHeader>
<DialogTitle>Revoke All Other Sessions?</DialogTitle>
<DialogDescription>
This will sign out all devices except the one you&apos;re currently using.
You&apos;ll need to sign in again on those devices.
This will sign out all devices except the one you&apos;re currently using. You&apos;ll
need to sign in again on those devices.
</DialogDescription>
</DialogHeader>
<div className="py-4">

View File

@@ -22,11 +22,7 @@ export function ThemeToggle() {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Toggle theme">
{resolvedTheme === 'dark' ? (
<Moon className="h-5 w-5" />
) : (
<Sun className="h-5 w-5" />
)}
{resolvedTheme === 'dark' ? <Moon className="h-5 w-5" /> : <Sun className="h-5 w-5" />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">

View File

@@ -1,31 +1,23 @@
"use client"
'use client';
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { cn } from "@/lib/utils/index"
import { buttonVariants } from "@/components/ui/button"
import { cn } from '@/lib/utils/index';
import { buttonVariants } from '@/components/ui/button';
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
}
function AlertDialogOverlay({
@@ -36,12 +28,12 @@ function AlertDialogOverlay({
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className
)}
{...props}
/>
)
);
}
function AlertDialogContent({
@@ -54,42 +46,33 @@ function AlertDialogContent({
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
className
)}
{...props}
/>
</AlertDialogPortal>
)
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
{...props}
/>
)
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
)
);
}
function AlertDialogTitle({
@@ -99,10 +82,10 @@ function AlertDialogTitle({
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
className={cn('text-lg font-semibold', className)}
{...props}
/>
)
);
}
function AlertDialogDescription({
@@ -112,22 +95,17 @@ function AlertDialogDescription({
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
)
);
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
return <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />;
}
function AlertDialogCancel({
@@ -136,10 +114,10 @@ function AlertDialogCancel({
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
className={cn(buttonVariants({ variant: 'outline' }), className)}
{...props}
/>
)
);
}
export {
@@ -154,4 +132,4 @@ export {
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
};

View File

@@ -1,59 +1,49 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
{
variants: {
variant: {
default: "bg-background text-foreground",
default: 'bg-background text-foreground',
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: "default",
variant: 'default',
},
}
)
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
));
Alert.displayName = 'Alert';
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
{...props}
/>
)
);
AlertTitle.displayName = 'AlertTitle';
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
));
AlertDescription.displayName = 'AlertDescription';
export { Alert, AlertTitle, AlertDescription }
export { Alert, AlertTitle, AlertDescription };

View File

@@ -1,9 +1,9 @@
"use client"
'use client';
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -11,14 +11,11 @@ const Avatar = React.forwardRef<
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -26,11 +23,11 @@ const AvatarImage = React.forwardRef<
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -39,12 +36,12 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
'flex h-full w-full items-center justify-center rounded-full bg-muted',
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback }
export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -1,36 +1,33 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: "default",
variant: 'default',
},
}
)
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants }
export { Badge, badgeVariants };

View File

@@ -1,57 +1,50 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "@/lib/utils/index"
import { cn } from '@/lib/utils/index';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: "default",
size: "default",
variant: 'default',
size: 'default',
},
}
)
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
);
}
)
Button.displayName = "Button"
);
Button.displayName = 'Button';
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@@ -1,76 +1,55 @@
import * as React from "react"
import * as React from 'react';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('rounded-xl border bg-card text-card-foreground shadow', className)}
{...props}
/>
)
);
Card.displayName = 'Card';
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
)
);
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('font-semibold leading-none tracking-tight', className)}
{...props}
/>
)
);
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
)
);
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
)
);
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
)
);
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View File

@@ -1,9 +1,9 @@
"use client"
'use client';
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { cn } from "@/lib/utils"
import { CheckIcon } from "@radix-ui/react-icons"
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { cn } from '@/lib/utils';
import { CheckIcon } from '@radix-ui/react-icons';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
@@ -12,18 +12,16 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
'grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("grid place-content-center text-current")}
>
<CheckboxPrimitive.Indicator className={cn('grid place-content-center text-current')}>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox }
export { Checkbox };

View File

@@ -1,17 +1,17 @@
"use client"
'use client';
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { cn } from "@/lib/utils"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { cn } from '@/lib/utils';
import { Cross2Icon } from '@radix-ui/react-icons';
const Dialog = DialogPrimitive.Root
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -20,13 +20,13 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@@ -37,7 +37,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
@@ -49,36 +49,21 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
);
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
);
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@@ -86,14 +71,11 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@@ -101,11 +83,11 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
@@ -118,4 +100,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
}
};

View File

@@ -1,33 +1,33 @@
"use client"
'use client';
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { cn } from "@/lib/utils"
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { cn } from '@/lib/utils';
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from '@radix-ui/react-icons';
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
'flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
inset && 'pl-8',
className
)}
{...props}
@@ -35,9 +35,8 @@ const DropdownMenuSubTrigger = React.forwardRef<
{children}
<ChevronRightIcon className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -46,14 +45,13 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]',
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -64,33 +62,33 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
'z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -99,7 +97,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
@@ -112,9 +110,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -123,7 +120,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
@@ -135,26 +132,22 @@ const DropdownMenuRadioItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@@ -162,24 +155,18 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
<span className={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...props} />
);
};
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export {
DropdownMenu,
@@ -197,4 +184,4 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
};

View File

@@ -1,22 +1,22 @@
import * as React from "react"
import * as React from 'react';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
ref={ref}
{...props}
/>
)
);
}
)
Input.displayName = "Input"
);
Input.displayName = 'Input';
export { Input }
export { Input };

View File

@@ -1,26 +1,21 @@
"use client"
'use client';
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }
export { Label };

View File

@@ -1,33 +1,33 @@
"use client"
'use client';
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const Popover = PopoverPrimitive.Root
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]',
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@@ -1,15 +1,15 @@
"use client"
'use client';
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { cn } from "@/lib/utils"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons"
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
import { cn } from '@/lib/utils';
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
const Select = SelectPrimitive.Root
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className
)}
{...props}
@@ -28,8 +28,8 @@ const SelectTrigger = React.forwardRef<
<ChevronDownIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
@@ -37,16 +37,13 @@ const SelectScrollUpButton = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronUpIcon className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
@@ -54,29 +51,25 @@ const SelectScrollDownButton = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronDownIcon className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
'relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
@@ -85,9 +78,9 @@ const SelectContent = React.forwardRef<
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
)}
>
{children}
@@ -95,8 +88,8 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
@@ -104,11 +97,11 @@ const SelectLabel = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
@@ -117,7 +110,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
@@ -129,8 +122,8 @@ const SelectItem = React.forwardRef<
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
@@ -138,11 +131,11 @@ const SelectSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
@@ -155,4 +148,4 @@ export {
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
};

View File

@@ -1,31 +1,26 @@
"use client"
'use client';
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator }
export { Separator };

View File

@@ -1,18 +1,18 @@
"use client"
'use client';
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as React from 'react';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { Cross2Icon } from '@radix-ui/react-icons';
const Sheet = SheetPrimitive.Root
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
@@ -20,33 +20,33 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: {
side: "right",
side: 'right',
},
}
)
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
@@ -55,14 +55,10 @@ interface SheetContentProps
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
@@ -70,36 +66,21 @@ const SheetContent = React.forwardRef<
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
);
SheetHeader.displayName = 'SheetHeader';
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
);
SheetFooter.displayName = 'SheetFooter';
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
@@ -107,11 +88,11 @@ const SheetTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
@@ -119,11 +100,11 @@ const SheetDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
@@ -136,4 +117,4 @@ export {
SheetFooter,
SheetTitle,
SheetDescription,
}
};

View File

@@ -1,15 +1,7 @@
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('animate-pulse rounded-md bg-primary/10', className)} {...props} />;
}
export { Skeleton }
export { Skeleton };

View File

@@ -1,31 +1,29 @@
"use client"
'use client';
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
import { useTheme } from 'next-themes';
import { Toaster as Sonner } from 'sonner';
type ToasterProps = React.ComponentProps<typeof Sonner>
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
const { theme = 'system' } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
theme={theme as ToasterProps['theme']}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}}
{...props}
/>
)
}
);
};
export { Toaster }
export { Toaster };

View File

@@ -1,40 +1,31 @@
import * as React from "react"
import * as React from 'react';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
</div>
)
);
Table.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
));
TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
@@ -42,29 +33,25 @@ const TableFooter = React.forwardRef<
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
));
TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className
)}
{...props}
/>
)
);
TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef<
HTMLTableCellElement,
@@ -73,13 +60,13 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
));
TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef<
HTMLTableCellElement,
@@ -88,33 +75,20 @@ const TableCell = React.forwardRef<
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
));
TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
));
TableCaption.displayName = 'TableCaption';
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };

View File

@@ -1,11 +1,11 @@
"use client"
'use client';
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const Tabs = TabsPrimitive.Root
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
@@ -14,13 +14,13 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
@@ -29,13 +29,13 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
@@ -44,12 +44,12 @@ const TabsContent = React.forwardRef<
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent }
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -1,22 +1,21 @@
import * as React from "react"
import * as React from 'react';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<'textarea'>>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
ref={ref}
{...props}
/>
);
}
);
Textarea.displayName = 'Textarea';
export { Textarea }
export { Textarea };

Some files were not shown because too many files have changed in this diff Show More