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:
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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'll send you an email with instructions to reset your password
|
||||
</p>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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="alert"</code> to error messages</span>
|
||||
<span>
|
||||
Add <code className="text-xs">role="alert"</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">
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 || ' '}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 '';
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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't a boilerplate generator or a paid SaaS template. It's a complete,
|
||||
production-ready codebase you can clone and customize. Everything you need to build
|
||||
|
||||
@@ -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's features. Both accounts are pre-configured with sample data.
|
||||
Use these credentials to explore the template's features. Both accounts are
|
||||
pre-configured with sample data.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -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's capabilities with live demos. Login with demo credentials to test features.
|
||||
Explore the template'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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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'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'll need to sign in again to access your
|
||||
account from it. This action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
|
||||
@@ -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't
|
||||
recognize, revoke it immediately and change your password.
|
||||
<strong className="text-foreground">Security tip:</strong> If you see a session you
|
||||
don'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're currently using.
|
||||
You'll need to sign in again on those devices.
|
||||
This will sign out all devices except the one you're currently using. You'll
|
||||
need to sign in again on those devices.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user