forked from cardosofelipe/fast-next-template
Remove all obsolete authentication, settings, admin, and demo-related components and pages
- Eliminated redundant components, pages, and layouts related to authentication (`login`, `register`, `password-reset`, etc.), user settings, admin, and demos. - Simplified the frontend structure by removing unused dynamic imports, forms, and test code. - Refactored configurations and metadata imports to exclude references to removed features. - Streamlined the project for future development and improved maintainability by discarding legacy and unused code.
This commit is contained in:
10
frontend/src/app/[locale]/(auth)/layout.tsx
Normal file
10
frontend/src/app/[locale]/(auth)/layout.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { AuthLayoutClient } from '@/components/auth/AuthLayoutClient';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Authentication',
|
||||
};
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return <AuthLayoutClient>{children}</AuthLayoutClient>;
|
||||
}
|
||||
31
frontend/src/app/[locale]/(auth)/login/page.tsx
Normal file
31
frontend/src/app/[locale]/(auth)/login/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// Code-split LoginForm - heavy with react-hook-form + validation
|
||||
const LoginForm = dynamic(
|
||||
/* istanbul ignore next - Next.js dynamic import, tested via component */
|
||||
() => import('@/components/auth/LoginForm').then((mod) => ({ default: mod.LoginForm })),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="animate-pulse h-10 bg-muted rounded" />
|
||||
<div className="animate-pulse h-10 bg-muted rounded" />
|
||||
<div className="animate-pulse h-10 bg-primary/20 rounded" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
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>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Access your dashboard and manage your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LoginForm showRegisterLink showPasswordResetLink />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Password Reset Confirm Content Component
|
||||
* Wrapped in Suspense boundary to handle useSearchParams()
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useRouter } from '@/lib/i18n/routing';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Alert } from '@/components/ui/alert';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
|
||||
// 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,
|
||||
})),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="animate-pulse h-10 bg-muted rounded" />
|
||||
<div className="animate-pulse h-10 bg-muted rounded" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
export default function PasswordResetConfirmContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const token = searchParams.get('token');
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle successful password reset
|
||||
const handleSuccess = () => {
|
||||
// Wait 3 seconds then redirect to login
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
router.push('/login');
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// If no token in URL, show error
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<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.
|
||||
</p>
|
||||
</Alert>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="/password-reset"
|
||||
className="text-sm text-primary underline-offset-4 hover:underline font-medium"
|
||||
>
|
||||
Request new reset link
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
Choose a strong password for your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PasswordResetConfirmForm token={token} onSuccess={handleSuccess} showLoginLink />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Password Reset Confirm Page
|
||||
* Users set a new password using the token from their email
|
||||
*/
|
||||
|
||||
import { Suspense } from 'react';
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PasswordResetConfirmContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
38
frontend/src/app/[locale]/(auth)/password-reset/page.tsx
Normal file
38
frontend/src/app/[locale]/(auth)/password-reset/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Password Reset Request Page
|
||||
* Users enter their email to receive reset instructions
|
||||
*/
|
||||
|
||||
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,
|
||||
})),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="animate-pulse h-10 bg-muted rounded" />
|
||||
<div className="animate-pulse h-10 bg-primary/20 rounded" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
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>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
We'll send you an email with instructions to reset your password
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PasswordResetRequestForm showLoginLink />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
frontend/src/app/[locale]/(auth)/register/page.tsx
Normal file
31
frontend/src/app/[locale]/(auth)/register/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// Code-split RegisterForm (313 lines)
|
||||
const RegisterForm = dynamic(
|
||||
/* istanbul ignore next - Next.js dynamic import, tested via component */
|
||||
() => import('@/components/auth/RegisterForm').then((mod) => ({ default: mod.RegisterForm })),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="animate-pulse h-10 bg-muted rounded" />
|
||||
<div className="animate-pulse h-10 bg-muted rounded" />
|
||||
<div className="animate-pulse h-10 bg-muted rounded" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
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>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Get started with your free account today
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<RegisterForm showLoginLink />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
frontend/src/app/[locale]/(authenticated)/layout.tsx
Normal file
28
frontend/src/app/[locale]/(authenticated)/layout.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Authenticated Route Group Layout
|
||||
* Wraps all authenticated routes with AuthGuard and provides common layout structure
|
||||
*/
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import { AuthGuard } from '@/components/auth';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '%s | FastNext Template',
|
||||
default: 'Dashboard',
|
||||
},
|
||||
};
|
||||
|
||||
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>
|
||||
<Footer />
|
||||
</div>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Settings Layout
|
||||
* Provides tabbed navigation for settings pages
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { usePathname } from '@/lib/i18n/routing';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { User, Lock, Monitor, Settings as SettingsIcon } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Settings tabs configuration
|
||||
*/
|
||||
const settingsTabs = [
|
||||
{
|
||||
value: 'profile',
|
||||
label: 'Profile',
|
||||
href: '/settings/profile',
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
value: 'password',
|
||||
label: 'Password',
|
||||
href: '/settings/password',
|
||||
icon: Lock,
|
||||
},
|
||||
{
|
||||
value: 'sessions',
|
||||
label: 'Sessions',
|
||||
href: '/settings/sessions',
|
||||
icon: Monitor,
|
||||
},
|
||||
{
|
||||
value: 'preferences',
|
||||
label: 'Preferences',
|
||||
href: '/settings/preferences',
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
];
|
||||
|
||||
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Determine active tab based on pathname
|
||||
const activeTab = settingsTabs.find((tab) => pathname.startsWith(tab.href))?.value || 'profile';
|
||||
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<Tabs value={activeTab} className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-2 md:grid-cols-4 lg:w-[600px]">
|
||||
{settingsTabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<TabsTrigger key={tab.value} value={tab.value} asChild>
|
||||
<Link href={tab.href} className="flex items-center space-x-2">
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{tab.label}</span>
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="rounded-lg border bg-card text-card-foreground p-6">{children}</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
frontend/src/app/[locale]/(authenticated)/settings/page.tsx
Normal file
11
frontend/src/app/[locale]/(authenticated)/settings/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Settings Index Page
|
||||
* Redirects to /settings/profile
|
||||
*/
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function SettingsPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
redirect(`/${locale}/settings/profile`);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Password Settings Page
|
||||
* Secure password change functionality for authenticated users
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { PasswordChangeForm } from '@/components/settings';
|
||||
|
||||
export default function PasswordSettingsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<PasswordChangeForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* User Preferences Page
|
||||
* Theme, notifications, and other preferences
|
||||
*/
|
||||
|
||||
/* istanbul ignore next - Next.js type import for metadata */
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
/* istanbul ignore next - Next.js metadata, not executable code */
|
||||
export const metadata: Metadata = {
|
||||
title: 'Preferences',
|
||||
};
|
||||
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Profile Settings Page
|
||||
* User profile management - edit name, email, and other profile information
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { ProfileSettingsForm } from '@/components/settings';
|
||||
|
||||
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>
|
||||
</div>
|
||||
|
||||
<ProfileSettingsForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Session Management Page
|
||||
* View and manage active sessions across all devices
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { SessionsManager } from '@/components/settings';
|
||||
|
||||
export default function SessionsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<SessionsManager />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
frontend/src/app/[locale]/admin/layout.tsx
Normal file
44
frontend/src/app/[locale]/admin/layout.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Admin Route Group Layout
|
||||
* Wraps all admin routes with AuthGuard requiring superuser privileges
|
||||
* Includes sidebar navigation and breadcrumbs
|
||||
*/
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import { AuthGuard } from '@/components/auth';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
import { AdminSidebar, Breadcrumbs } from '@/components/admin';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '%s | Admin | FastNext Template',
|
||||
default: 'Admin Dashboard',
|
||||
},
|
||||
};
|
||||
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<AuthGuard requireAdmin>
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-primary-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<Header />
|
||||
<div className="flex flex-1">
|
||||
<AdminSidebar />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<Breadcrumbs />
|
||||
<main id="main-content" className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Admin Organization Members Page
|
||||
* Displays and manages members of a specific organization
|
||||
* Protected by AuthGuard in layout with requireAdmin=true
|
||||
*/
|
||||
|
||||
/* istanbul ignore next - Next.js type import for metadata */
|
||||
import type { Metadata } from 'next';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { OrganizationMembersContent } from '@/components/admin/organizations/OrganizationMembersContent';
|
||||
|
||||
/* istanbul ignore next - Next.js metadata, not executable code */
|
||||
export const metadata: Metadata = {
|
||||
title: 'Organization Members',
|
||||
};
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function OrganizationMembersPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<div className="space-y-6">
|
||||
{/* Back Button */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin/organizations">
|
||||
<Button variant="outline" size="icon" aria-label="Back to Organizations">
|
||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Organization Members Content */}
|
||||
<OrganizationMembersContent organizationId={id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
frontend/src/app/[locale]/admin/organizations/page.tsx
Normal file
37
frontend/src/app/[locale]/admin/organizations/page.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Admin Organizations Page
|
||||
* Displays and manages all organizations
|
||||
* Protected by AuthGuard in layout with requireAdmin=true
|
||||
*/
|
||||
|
||||
/* istanbul ignore next - Next.js type import for metadata */
|
||||
import type { Metadata } from 'next';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { OrganizationManagementContent } from '@/components/admin/organizations/OrganizationManagementContent';
|
||||
|
||||
/* istanbul ignore next - Next.js metadata, not executable code */
|
||||
export const metadata: Metadata = {
|
||||
title: 'Organizations',
|
||||
};
|
||||
|
||||
export default function AdminOrganizationsPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<div className="space-y-6">
|
||||
{/* Back Button */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin">
|
||||
<Button variant="outline" size="icon" aria-label="Back to Admin Dashboard">
|
||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Organization Management Content */}
|
||||
<OrganizationManagementContent />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
92
frontend/src/app/[locale]/admin/page.tsx
Normal file
92
frontend/src/app/[locale]/admin/page.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Admin Dashboard Page
|
||||
* Displays admin statistics and management options
|
||||
* Protected by AuthGuard in layout with requireAdmin=true
|
||||
*/
|
||||
|
||||
/* istanbul ignore next - Next.js type import for metadata */
|
||||
import type { Metadata } from 'next';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import { DashboardStats } from '@/components/admin';
|
||||
import {
|
||||
UserGrowthChart,
|
||||
OrganizationDistributionChart,
|
||||
SessionActivityChart,
|
||||
UserStatusChart,
|
||||
} from '@/components/charts';
|
||||
import { Users, Building2, Settings } from 'lucide-react';
|
||||
|
||||
/* istanbul ignore next - Next.js metadata, not executable code */
|
||||
export const metadata: Metadata = {
|
||||
title: 'Admin Dashboard',
|
||||
};
|
||||
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<div className="space-y-8">
|
||||
{/* Page Header */}
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<DashboardStats />
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Quick Actions</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Link href="/admin/users" className="block">
|
||||
<div className="rounded-lg border bg-card p-6 transition-colors hover:bg-accent cursor-pointer">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Users className="h-5 w-5 text-primary" aria-hidden="true" />
|
||||
<h3 className="font-semibold">User Management</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
View, create, and manage user accounts
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/organizations" className="block">
|
||||
<div className="rounded-lg border bg-card p-6 transition-colors hover:bg-accent cursor-pointer">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Building2 className="h-5 w-5 text-primary" aria-hidden="true" />
|
||||
<h3 className="font-semibold">Organizations</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage organizations and their members
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/settings" className="block">
|
||||
<div className="rounded-lg border bg-card p-6 transition-colors hover:bg-accent cursor-pointer">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<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>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Analytics Charts */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Analytics Overview</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<UserGrowthChart />
|
||||
<SessionActivityChart />
|
||||
<OrganizationDistributionChart />
|
||||
<UserStatusChart />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
frontend/src/app/[locale]/admin/settings/page.tsx
Normal file
56
frontend/src/app/[locale]/admin/settings/page.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Admin Settings Page
|
||||
* System-wide settings and configuration
|
||||
* Protected by AuthGuard in layout with requireAdmin=true
|
||||
*/
|
||||
|
||||
/* istanbul ignore next - Next.js type import for metadata */
|
||||
import type { Metadata } from 'next';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
/* istanbul ignore next - Next.js metadata, not executable code */
|
||||
export const metadata: Metadata = {
|
||||
title: 'System Settings',
|
||||
};
|
||||
|
||||
export default function AdminSettingsPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<div className="space-y-6">
|
||||
{/* Back Button + Header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin">
|
||||
<Button variant="outline" size="icon" aria-label="Back to Admin Dashboard">
|
||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<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:</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>
|
||||
<li>• Security and authentication options</li>
|
||||
<li>• API and integration settings</li>
|
||||
<li>• Maintenance and backup tools</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
frontend/src/app/[locale]/admin/users/page.tsx
Normal file
41
frontend/src/app/[locale]/admin/users/page.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Admin Users Page
|
||||
* Displays and manages all users
|
||||
* Protected by AuthGuard in layout with requireAdmin=true
|
||||
*/
|
||||
|
||||
/* istanbul ignore next - Next.js type import for metadata */
|
||||
import type { Metadata } from 'next';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { UserManagementContent } from '@/components/admin/users/UserManagementContent';
|
||||
|
||||
/* istanbul ignore next - Next.js metadata, not executable code */
|
||||
export const metadata: Metadata = {
|
||||
title: 'User Management',
|
||||
};
|
||||
|
||||
export default function AdminUsersPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<div className="space-y-6">
|
||||
{/* Back Button + Header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin">
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Management Content */}
|
||||
<UserManagementContent />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
379
frontend/src/app/[locale]/demos/page.tsx
Normal file
379
frontend/src/app/[locale]/demos/page.tsx
Normal file
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* Demo Tour Page
|
||||
* Comprehensive guide to all template features and demos
|
||||
*/
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import {
|
||||
Palette,
|
||||
ShieldCheck,
|
||||
UserCircle,
|
||||
Play,
|
||||
CheckCircle2,
|
||||
ArrowRight,
|
||||
Home,
|
||||
LogIn,
|
||||
Settings,
|
||||
Users,
|
||||
Activity,
|
||||
UserCog,
|
||||
BarChart3,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Demo Tour | FastNext Template',
|
||||
description:
|
||||
'Try all features with demo credentials - comprehensive guide to the FastNext template',
|
||||
};
|
||||
|
||||
const demoCategories = [
|
||||
{
|
||||
icon: Palette,
|
||||
title: 'Design System Hub',
|
||||
description: 'Browse components, layouts, spacing, and forms with live examples',
|
||||
href: '/dev',
|
||||
features: [
|
||||
'All UI components',
|
||||
'Layout patterns',
|
||||
'Spacing philosophy',
|
||||
'Form implementations',
|
||||
],
|
||||
credentials: null,
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: 'Authentication System',
|
||||
description: 'Test login, registration, password reset, and session management',
|
||||
href: '/login',
|
||||
features: ['Login & logout', 'Registration', 'Password reset', 'Session tokens'],
|
||||
credentials: {
|
||||
email: 'demo@example.com',
|
||||
password: 'Demo123!',
|
||||
role: 'Regular User',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: UserCircle,
|
||||
title: 'User Features',
|
||||
description: 'Experience user settings, profile management, and session monitoring',
|
||||
href: '/settings',
|
||||
features: ['Profile editing', 'Password changes', 'Active sessions', 'Preferences'],
|
||||
credentials: {
|
||||
email: 'demo@example.com',
|
||||
password: 'Demo123!',
|
||||
role: 'Regular User',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Play,
|
||||
title: 'Admin Dashboard',
|
||||
description: 'Explore admin panel with user management and analytics',
|
||||
href: '/admin',
|
||||
features: ['User management', 'Analytics charts', 'Bulk operations', 'Organization control'],
|
||||
credentials: {
|
||||
email: 'admin@example.com',
|
||||
password: 'Admin123!',
|
||||
role: 'Admin',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const explorationPaths = [
|
||||
{
|
||||
title: 'Quick Tour (5 min)',
|
||||
steps: [
|
||||
{ icon: Home, text: 'Browse Design System components', href: '/dev' },
|
||||
{ icon: LogIn, text: 'Test login flow', href: '/login' },
|
||||
{ icon: Settings, text: 'View user settings', href: '/settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Full Experience (15 min)',
|
||||
steps: [
|
||||
{ icon: Palette, text: 'Explore all design system pages', href: '/dev' },
|
||||
{ icon: ShieldCheck, text: 'Try complete auth flow', href: '/login' },
|
||||
{ icon: UserCog, text: 'Update profile and password', href: '/settings/profile' },
|
||||
{ icon: Activity, text: 'Check active sessions', href: '/settings/sessions' },
|
||||
{ icon: Users, text: 'Login as admin and manage users', href: '/admin/users' },
|
||||
{ icon: BarChart3, text: 'View analytics dashboard', href: '/admin' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const checklist = [
|
||||
{ label: 'Browse design system components', completed: false },
|
||||
{ label: 'Test login/logout flow', completed: false },
|
||||
{ label: 'Register a new account', completed: false },
|
||||
{ label: 'Reset password', completed: false },
|
||||
{ label: 'Update user profile', completed: false },
|
||||
{ label: 'Change password', completed: false },
|
||||
{ label: 'View active sessions', completed: false },
|
||||
{ label: 'Login as admin', completed: false },
|
||||
{ label: 'Manage users (admin)', completed: false },
|
||||
{ label: 'View analytics (admin)', completed: false },
|
||||
{ label: 'Perform bulk operations (admin)', completed: false },
|
||||
{ label: 'Explore organizations (admin)', completed: false },
|
||||
];
|
||||
|
||||
export default function DemoTourPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex h-14 items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link href="/" className="gap-2">
|
||||
<Home className="h-4 w-4" />
|
||||
Home
|
||||
</Link>
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
<h1 className="text-lg font-semibold">Demo Tour</h1>
|
||||
</div>
|
||||
<Button asChild variant="default" size="sm">
|
||||
<Link href="/login">Start Exploring →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="container mx-auto px-4 py-12">
|
||||
<div className="space-y-12 max-w-6xl mx-auto">
|
||||
{/* Hero Section */}
|
||||
<section className="text-center space-y-4">
|
||||
<Badge variant="secondary" className="mb-2">
|
||||
Interactive Demo
|
||||
</Badge>
|
||||
<h2 className="text-4xl font-bold tracking-tight">Explore All Features</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Try everything with our pre-configured demo accounts. All changes are safe to test and
|
||||
reset automatically.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Quick Start Guide */}
|
||||
<section className="space-y-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-semibold mb-2">Quick Start Guide</h3>
|
||||
<p className="text-muted-foreground">Follow these simple steps to get started</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary text-primary-foreground font-bold">
|
||||
1
|
||||
</div>
|
||||
<CardTitle className="text-lg">Choose a Demo</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Browse the demo categories below and pick what interests you most
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary text-primary-foreground font-bold">
|
||||
2
|
||||
</div>
|
||||
<CardTitle className="text-lg">Use Credentials</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Copy the demo credentials and login to explore authenticated features
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary text-primary-foreground font-bold">
|
||||
3
|
||||
</div>
|
||||
<CardTitle className="text-lg">Explore Freely</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Test all features - everything resets automatically, so experiment away!
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Demo Categories */}
|
||||
<section className="space-y-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-semibold mb-2">Demo Categories</h3>
|
||||
<p className="text-muted-foreground">Explore different areas of the template</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{demoCategories.map((category) => {
|
||||
const Icon = category.icon;
|
||||
return (
|
||||
<Card key={category.title} className="relative">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="rounded-lg bg-primary/10 p-2">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
{category.credentials && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Login Required
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<CardTitle>{category.title}</CardTitle>
|
||||
<CardDescription>{category.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Features List */}
|
||||
<div className="space-y-2">
|
||||
{category.features.map((feature) => (
|
||||
<div key={feature} className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-primary flex-shrink-0" />
|
||||
<span className="text-muted-foreground">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Credentials */}
|
||||
{category.credentials && (
|
||||
<div className="rounded-md bg-muted p-3 space-y-1">
|
||||
<p className="text-xs font-semibold text-muted-foreground">
|
||||
{category.credentials.role} Credentials:
|
||||
</p>
|
||||
<p className="font-mono text-sm">{category.credentials.email}</p>
|
||||
<p className="font-mono text-sm">{category.credentials.password}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CTA */}
|
||||
<Button asChild className="w-full gap-2">
|
||||
<Link href={category.href}>
|
||||
{category.credentials ? 'Try Now' : 'Explore'}{' '}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Suggested Exploration Paths */}
|
||||
<section className="space-y-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-semibold mb-2">Suggested Exploration Paths</h3>
|
||||
<p className="text-muted-foreground">Choose your adventure based on available time</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{explorationPaths.map((path) => (
|
||||
<Card key={path.title}>
|
||||
<CardHeader>
|
||||
<CardTitle>{path.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{path.steps.map((step, index) => {
|
||||
const StepIcon = step.icon;
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
href={step.href}
|
||||
className="flex items-center gap-3 p-2 rounded-lg hover:bg-accent transition-colors group"
|
||||
>
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary/10 text-primary text-xs font-bold">
|
||||
{index + 1}
|
||||
</div>
|
||||
<StepIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm flex-1 group-hover:text-primary transition-colors">
|
||||
{step.text}
|
||||
</span>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* What to Try Checklist */}
|
||||
<section className="space-y-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-semibold mb-2">What to Try</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Complete checklist of features to explore (for your reference)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Feature Checklist</CardTitle>
|
||||
<CardDescription>
|
||||
Try these features to experience the full power of the template
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
{checklist.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-3 text-sm">
|
||||
<div className="flex-shrink-0 w-5 h-5 rounded border border-muted-foreground/30 flex items-center justify-center">
|
||||
{item.completed && <CheckCircle2 className="h-4 w-4 text-primary" />}
|
||||
</div>
|
||||
<span className={item.completed ? 'text-muted-foreground line-through' : ''}>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="text-center space-y-6 py-8">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-2xl font-semibold">Ready to Start?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Pick a demo category above or jump right into the action
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/login">Try Authentication Flow →</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/dev">Browse Design System</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
frontend/src/app/[locale]/dev/components/page.tsx
Normal file
27
frontend/src/app/[locale]/dev/components/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Component Showcase Page
|
||||
* Development-only page to preview all shadcn/ui components
|
||||
* Access: /dev/components
|
||||
*/
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// Code-split heavy dev component (787 lines)
|
||||
const ComponentShowcase = dynamic(
|
||||
() => import('@/components/dev/ComponentShowcase').then((mod) => mod.ComponentShowcase),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="p-8 text-center text-muted-foreground">Loading components...</div>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Component Showcase | Dev',
|
||||
description: 'Preview all design system components',
|
||||
};
|
||||
|
||||
export default function ComponentShowcasePage() {
|
||||
return <ComponentShowcase />;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Dynamic Documentation Route
|
||||
* Renders markdown files from docs/ directory
|
||||
* Access: /dev/docs/design-system/01-foundations, etc.
|
||||
*/
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { MarkdownContent } from '@/components/docs/MarkdownContent';
|
||||
import { DevBreadcrumbs } from '@/components/dev/DevBreadcrumbs';
|
||||
|
||||
interface DocPageProps {
|
||||
params: Promise<{ slug: string[] }>;
|
||||
}
|
||||
|
||||
// Generate static params for all documentation files
|
||||
export async function generateStaticParams() {
|
||||
const docsDir = path.join(process.cwd(), 'docs', 'design-system');
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(docsDir);
|
||||
const mdFiles = files.filter((file) => file.endsWith('.md'));
|
||||
|
||||
return mdFiles.map((file) => ({
|
||||
slug: [file.replace(/\.md$/, '')],
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Get markdown file content
|
||||
async function getDocContent(slug: string[]) {
|
||||
const filePath = path.join(process.cwd(), 'docs', 'design-system', ...slug) + '.md';
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
const { data, content } = matter(fileContent);
|
||||
|
||||
return {
|
||||
frontmatter: data,
|
||||
content,
|
||||
filePath: slug.join('/'),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function DocPage({ params }: DocPageProps) {
|
||||
const { slug } = await params;
|
||||
const doc = await getDocContent(slug);
|
||||
|
||||
if (!doc) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Extract title from first heading or use filename
|
||||
const title = doc.content.match(/^#\s+(.+)$/m)?.[1] || slug[slug.length - 1];
|
||||
|
||||
return (
|
||||
<div className="bg-background">
|
||||
{/* Breadcrumbs */}
|
||||
<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">
|
||||
<MarkdownContent content={doc.content} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
261
frontend/src/app/[locale]/dev/docs/page.tsx
Normal file
261
frontend/src/app/[locale]/dev/docs/page.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Documentation Hub
|
||||
* Central hub for all design system documentation
|
||||
* Access: /dev/docs
|
||||
*/
|
||||
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
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';
|
||||
import { DevBreadcrumbs } from '@/components/dev/DevBreadcrumbs';
|
||||
|
||||
interface DocItem {
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
const gettingStartedDocs: DocItem[] = [
|
||||
{
|
||||
title: 'Quick Start',
|
||||
description: '5-minute crash course to get up and running with the design system',
|
||||
href: '/dev/docs/design-system/00-quick-start',
|
||||
icon: <Sparkles className="h-5 w-5" />,
|
||||
badge: 'Start Here',
|
||||
},
|
||||
{
|
||||
title: 'README',
|
||||
description: 'Complete overview and learning paths for the design system',
|
||||
href: '/dev/docs/design-system/README',
|
||||
icon: <BookOpen className="h-5 w-5" />,
|
||||
},
|
||||
];
|
||||
|
||||
const coreConceptsDocs: DocItem[] = [
|
||||
{
|
||||
title: 'Foundations',
|
||||
description: 'Colors (OKLCH), typography, spacing, and shadows',
|
||||
href: '/dev/docs/design-system/01-foundations',
|
||||
icon: <Palette className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: 'Components',
|
||||
description: 'shadcn/ui component library guide and usage patterns',
|
||||
href: '/dev/docs/design-system/02-components',
|
||||
icon: <Code2 className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: 'Layouts',
|
||||
description: 'Layout patterns with Grid vs Flex decision trees',
|
||||
href: '/dev/docs/design-system/03-layouts',
|
||||
icon: <Layout className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: 'Spacing Philosophy',
|
||||
description: 'Parent-controlled spacing strategy and best practices',
|
||||
href: '/dev/docs/design-system/04-spacing-philosophy',
|
||||
icon: <FileCode className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: 'Component Creation',
|
||||
description: 'When to create vs compose components',
|
||||
href: '/dev/docs/design-system/05-component-creation',
|
||||
icon: <Code2 className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: 'Forms',
|
||||
description: 'Form patterns with react-hook-form and Zod validation',
|
||||
href: '/dev/docs/design-system/06-forms',
|
||||
icon: <FileCode className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: 'Accessibility',
|
||||
description: 'WCAG AA compliance, keyboard navigation, and screen readers',
|
||||
href: '/dev/docs/design-system/07-accessibility',
|
||||
icon: <Accessibility className="h-5 w-5" />,
|
||||
},
|
||||
];
|
||||
|
||||
const referencesDocs: DocItem[] = [
|
||||
{
|
||||
title: 'AI Guidelines',
|
||||
description: 'Rules and best practices for AI code generation',
|
||||
href: '/dev/docs/design-system/08-ai-guidelines',
|
||||
icon: <Lightbulb className="h-5 w-5" />,
|
||||
badge: 'AI',
|
||||
},
|
||||
{
|
||||
title: 'Quick Reference',
|
||||
description: 'Cheat sheet for quick lookups and common patterns',
|
||||
href: '/dev/docs/design-system/99-reference',
|
||||
icon: <Search className="h-5 w-5" />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function DocsHub() {
|
||||
return (
|
||||
<div className="bg-background">
|
||||
{/* Breadcrumbs */}
|
||||
<DevBreadcrumbs items={[{ label: 'Documentation' }]} />
|
||||
|
||||
{/* Hero Section */}
|
||||
<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>
|
||||
<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.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3 justify-center">
|
||||
<Link href="/dev/docs/design-system/00-quick-start">
|
||||
<Button size="lg" className="gap-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Get Started
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/dev/docs/design-system/README">
|
||||
<Button variant="outline" size="lg" className="gap-2">
|
||||
<BookOpen className="h-4 w-4" />
|
||||
Full Documentation
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/dev/components">
|
||||
<Button variant="outline" size="lg" className="gap-2">
|
||||
<Code2 className="h-4 w-4" />
|
||||
View Examples
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="container mx-auto px-4 py-12">
|
||||
<div className="mx-auto max-w-6xl space-y-16">
|
||||
{/* Getting Started Section */}
|
||||
<section>
|
||||
<div className="mb-6">
|
||||
<h3 className="text-2xl font-semibold tracking-tight mb-2">Getting Started</h3>
|
||||
<p className="text-muted-foreground">
|
||||
New to the design system? Start here for a quick introduction.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{gettingStartedDocs.map((doc) => (
|
||||
<Link key={doc.href} href={doc.href} className="group">
|
||||
<Card className="h-full transition-all hover:shadow-lg hover:border-primary/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-primary/10 p-2.5 text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
|
||||
{doc.icon}
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl">{doc.title}</CardTitle>
|
||||
{doc.badge && (
|
||||
<Badge variant="secondary" className="mt-1">
|
||||
{doc.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-base">{doc.description}</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Core Concepts Section */}
|
||||
<section>
|
||||
<div className="mb-6">
|
||||
<h3 className="text-2xl font-semibold tracking-tight mb-2">Core Concepts</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Deep dive into the fundamental concepts and patterns of the design system.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{coreConceptsDocs.map((doc) => (
|
||||
<Link key={doc.href} href={doc.href} className="group">
|
||||
<Card className="h-full transition-all hover:shadow-lg hover:border-primary/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="rounded-lg bg-primary/10 p-2.5 text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
|
||||
{doc.icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-lg">{doc.title}</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription>{doc.description}</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* References Section */}
|
||||
<section>
|
||||
<div className="mb-6">
|
||||
<h3 className="text-2xl font-semibold tracking-tight mb-2">References</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Quick references and specialized guides for specific use cases.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{referencesDocs.map((doc) => (
|
||||
<Link key={doc.href} href={doc.href} className="group">
|
||||
<Card className="h-full transition-all hover:shadow-lg hover:border-primary/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-primary/10 p-2.5 text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
|
||||
{doc.icon}
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl">{doc.title}</CardTitle>
|
||||
{doc.badge && (
|
||||
<Badge variant="secondary" className="mt-1">
|
||||
{doc.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-base">{doc.description}</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
561
frontend/src/app/[locale]/dev/forms/page.tsx
Normal file
561
frontend/src/app/[locale]/dev/forms/page.tsx
Normal file
@@ -0,0 +1,561 @@
|
||||
/**
|
||||
* Form Patterns Demo
|
||||
* Interactive demonstrations of form patterns with validation
|
||||
* Access: /dev/forms
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
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 { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Example, ExampleSection } from '@/components/dev/Example';
|
||||
import { BeforeAfter } from '@/components/dev/BeforeAfter';
|
||||
|
||||
// Example schemas
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
});
|
||||
|
||||
const contactSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
message: z.string().min(10, 'Message must be at least 10 characters'),
|
||||
category: z.string().min(1, 'Please select a category'),
|
||||
});
|
||||
|
||||
type LoginForm = z.infer<typeof loginSchema>;
|
||||
type ContactForm = z.infer<typeof contactSchema>;
|
||||
|
||||
export default function FormsPage() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
|
||||
// Login form
|
||||
const {
|
||||
register: registerLogin,
|
||||
handleSubmit: handleSubmitLogin,
|
||||
formState: { errors: errorsLogin },
|
||||
} = useForm<LoginForm>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
|
||||
// Contact form
|
||||
const {
|
||||
register: registerContact,
|
||||
handleSubmit: handleSubmitContact,
|
||||
formState: { errors: errorsContact },
|
||||
setValue: setValueContact,
|
||||
} = useForm<ContactForm>({
|
||||
resolver: zodResolver(contactSchema),
|
||||
});
|
||||
|
||||
const onSubmitLogin = async (data: LoginForm) => {
|
||||
setIsSubmitting(true);
|
||||
setSubmitSuccess(false);
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Login form data:', data);
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
setSubmitSuccess(true);
|
||||
};
|
||||
|
||||
const onSubmitContact = async (data: ContactForm) => {
|
||||
setIsSubmitting(true);
|
||||
setSubmitSuccess(false);
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Contact form data:', data);
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
setSubmitSuccess(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-background">
|
||||
{/* Breadcrumbs */}
|
||||
<DevBreadcrumbs items={[{ label: 'Forms' }]} />
|
||||
|
||||
{/* Content */}
|
||||
<main className="container mx-auto px-4 py-12">
|
||||
<div className="space-y-12">
|
||||
{/* 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.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">react-hook-form</Badge>
|
||||
<Badge variant="outline">Zod</Badge>
|
||||
<Badge variant="outline">Validation</Badge>
|
||||
<Badge variant="outline">ARIA</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Basic Form */}
|
||||
<ExampleSection
|
||||
id="basic-form"
|
||||
title="Basic Form with Validation"
|
||||
description="Login form with email and password validation"
|
||||
>
|
||||
<Example
|
||||
title="Login Form"
|
||||
description="Validates on submit, shows field-level errors"
|
||||
code={`const schema = z.object({
|
||||
email: z.string().email('Invalid email'),
|
||||
password: z.string().min(8, 'Min 8 chars'),
|
||||
});
|
||||
|
||||
const { register, handleSubmit, formState: { errors } } = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
{...register('email')}
|
||||
aria-invalid={!!errors.email}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-destructive">
|
||||
{errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? <Loader2 className="animate-spin" /> : 'Submit'}
|
||||
</Button>
|
||||
</form>`}
|
||||
>
|
||||
<div className="max-w-md mx-auto">
|
||||
<form onSubmit={handleSubmitLogin(onSubmitLogin)} className="space-y-4">
|
||||
{/* Email */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="login-email">Email</Label>
|
||||
<Input
|
||||
id="login-email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
{...registerLogin('email')}
|
||||
aria-invalid={!!errorsLogin.email}
|
||||
aria-describedby={errorsLogin.email ? 'login-email-error' : undefined}
|
||||
/>
|
||||
{errorsLogin.email && (
|
||||
<p id="login-email-error" className="text-sm text-destructive" role="alert">
|
||||
{errorsLogin.email.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="login-password">Password</Label>
|
||||
<Input
|
||||
id="login-password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
{...registerLogin('password')}
|
||||
aria-invalid={!!errorsLogin.password}
|
||||
aria-describedby={errorsLogin.password ? 'login-password-error' : undefined}
|
||||
/>
|
||||
{errorsLogin.password && (
|
||||
<p
|
||||
id="login-password-error"
|
||||
className="text-sm text-destructive"
|
||||
role="alert"
|
||||
>
|
||||
{errorsLogin.password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{isSubmitting ? 'Signing In...' : 'Sign In'}
|
||||
</Button>
|
||||
|
||||
{/* Success Message */}
|
||||
{submitSuccess && (
|
||||
<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>Form submitted successfully.</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Complete Form */}
|
||||
<ExampleSection
|
||||
id="complete-form"
|
||||
title="Complete Form Example"
|
||||
description="Contact form with multiple field types"
|
||||
>
|
||||
<Example
|
||||
title="Contact Form"
|
||||
description="Text, textarea, select, and validation"
|
||||
code={`const schema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
email: z.string().email('Invalid email'),
|
||||
message: z.string().min(10, 'Min 10 characters'),
|
||||
category: z.string().min(1, 'Select a category'),
|
||||
});
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* Input field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input {...register('name')} />
|
||||
{errors.name && <p className="text-sm text-destructive">{errors.name.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Textarea field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea {...register('message')} rows={4} />
|
||||
{errors.message && <p className="text-sm text-destructive">{errors.message.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Select field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Select onValueChange={(value) => setValue('category', value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="support">Support</SelectItem>
|
||||
<SelectItem value="sales">Sales</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>`}
|
||||
>
|
||||
<div className="max-w-md mx-auto">
|
||||
<form onSubmit={handleSubmitContact(onSubmitContact)} className="space-y-4">
|
||||
{/* Name */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contact-name">Name</Label>
|
||||
<Input
|
||||
id="contact-name"
|
||||
placeholder="John Doe"
|
||||
{...registerContact('name')}
|
||||
aria-invalid={!!errorsContact.name}
|
||||
/>
|
||||
{errorsContact.name && (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
{errorsContact.name.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contact-email">Email</Label>
|
||||
<Input
|
||||
id="contact-email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
{...registerContact('email')}
|
||||
aria-invalid={!!errorsContact.email}
|
||||
/>
|
||||
{errorsContact.email && (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
{errorsContact.email.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contact-category">Category</Label>
|
||||
<Select onValueChange={(value) => setValueContact('category', value)}>
|
||||
<SelectTrigger id="contact-category">
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="support">Support</SelectItem>
|
||||
<SelectItem value="sales">Sales</SelectItem>
|
||||
<SelectItem value="feedback">Feedback</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errorsContact.category && (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
{errorsContact.category.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contact-message">Message</Label>
|
||||
<Textarea
|
||||
id="contact-message"
|
||||
placeholder="Type your message here..."
|
||||
rows={4}
|
||||
{...registerContact('message')}
|
||||
aria-invalid={!!errorsContact.message}
|
||||
/>
|
||||
{errorsContact.message && (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
{errorsContact.message.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{isSubmitting ? 'Sending...' : 'Send Message'}
|
||||
</Button>
|
||||
|
||||
{/* Success Message */}
|
||||
{submitSuccess && (
|
||||
<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>
|
||||
</Alert>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Error States */}
|
||||
<ExampleSection
|
||||
id="error-states"
|
||||
title="Error State Handling"
|
||||
description="Proper ARIA attributes and visual feedback"
|
||||
>
|
||||
<BeforeAfter
|
||||
title="Error State Best Practices"
|
||||
description="Use aria-invalid and aria-describedby for accessibility"
|
||||
before={{
|
||||
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>
|
||||
<div className="h-10 rounded-md border border-destructive bg-background"></div>
|
||||
<p className="text-sm text-destructive">Invalid email address</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
after={{
|
||||
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>
|
||||
<div
|
||||
className="h-10 rounded-md border border-destructive bg-background"
|
||||
role="textbox"
|
||||
aria-invalid="true"
|
||||
aria-describedby="email-error"
|
||||
>
|
||||
<span className="sr-only">Email input with error</span>
|
||||
</div>
|
||||
<p id="email-error" className="text-sm text-destructive" role="alert">
|
||||
Invalid email address
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Error Handling Checklist</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5" />
|
||||
<span>Visual indicator (red border, icon)</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5" />
|
||||
<span>Clear error message text</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Loading States */}
|
||||
<ExampleSection
|
||||
id="loading-states"
|
||||
title="Loading States"
|
||||
description="Proper feedback during async operations"
|
||||
>
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Button Loading State</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<code className="text-xs block">
|
||||
{`<Button disabled={isLoading}>
|
||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{isLoading ? 'Saving...' : 'Save'}
|
||||
</Button>`}
|
||||
</code>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm">Save</Button>
|
||||
<Button size="sm" disabled>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Form Disabled State</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<code className="text-xs block mb-3">
|
||||
{`<fieldset disabled={isLoading}>
|
||||
<Input />
|
||||
<Button type="submit" />
|
||||
</fieldset>`}
|
||||
</code>
|
||||
<div className="space-y-2 opacity-60">
|
||||
<Input placeholder="Disabled input" disabled />
|
||||
<Button size="sm" disabled className="w-full">
|
||||
Disabled Button
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Zod Patterns */}
|
||||
<ExampleSection
|
||||
id="zod-patterns"
|
||||
title="Common Zod Validation Patterns"
|
||||
description="Reusable validation schemas"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Validation Pattern Library</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium text-sm">Required String</div>
|
||||
<code className="block rounded bg-muted p-2 text-xs">
|
||||
z.string().min(1, "Required")
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium text-sm">Email</div>
|
||||
<code className="block rounded bg-muted p-2 text-xs">
|
||||
z.string().email("Invalid email")
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium text-sm">Password (min length)</div>
|
||||
<code className="block rounded bg-muted p-2 text-xs">
|
||||
z.string().min(8, "Min 8 characters")
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium text-sm">Number Range</div>
|
||||
<code className="block rounded bg-muted p-2 text-xs">
|
||||
z.coerce.number().min(0).max(100)
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium text-sm">Password Confirmation</div>
|
||||
<code className="block rounded bg-muted p-2 text-xs">
|
||||
{`z.object({
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string()
|
||||
}).refine(data => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"]
|
||||
})`}
|
||||
</code>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ExampleSection>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-16 border-t py-6">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Learn more:{' '}
|
||||
<Link
|
||||
href="/dev/docs/design-system/06-forms"
|
||||
className="font-medium hover:text-foreground"
|
||||
>
|
||||
Forms Documentation
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
frontend/src/app/[locale]/dev/layout.tsx
Normal file
10
frontend/src/app/[locale]/dev/layout.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Dev Layout
|
||||
* Shared layout for all development routes
|
||||
*/
|
||||
|
||||
import { DevLayout } from '@/components/dev/DevLayout';
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <DevLayout>{children}</DevLayout>;
|
||||
}
|
||||
480
frontend/src/app/[locale]/dev/layouts/page.tsx
Normal file
480
frontend/src/app/[locale]/dev/layouts/page.tsx
Normal file
@@ -0,0 +1,480 @@
|
||||
/**
|
||||
* Layout Patterns Demo
|
||||
* Interactive demonstrations of essential layout patterns
|
||||
* Access: /dev/layouts
|
||||
*/
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
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 { Badge } from '@/components/ui/badge';
|
||||
import { Example, ExampleSection } from '@/components/dev/Example';
|
||||
import { BeforeAfter } from '@/components/dev/BeforeAfter';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Layout Patterns | Dev',
|
||||
description: 'Essential layout patterns with before/after examples',
|
||||
};
|
||||
|
||||
export default function LayoutsPage() {
|
||||
return (
|
||||
<div className="bg-background">
|
||||
{/* Breadcrumbs */}
|
||||
<DevBreadcrumbs items={[{ label: 'Layouts' }]} />
|
||||
|
||||
{/* Content */}
|
||||
<main className="container mx-auto px-4 py-12">
|
||||
<div className="space-y-12">
|
||||
{/* 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.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">Grid vs Flex</Badge>
|
||||
<Badge variant="outline">Responsive</Badge>
|
||||
<Badge variant="outline">Mobile-first</Badge>
|
||||
<Badge variant="outline">Best practices</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 1. Page Container */}
|
||||
<ExampleSection
|
||||
id="page-container"
|
||||
title="1. Page Container"
|
||||
description="Standard page layout with constrained width"
|
||||
>
|
||||
<Example
|
||||
title="Page Container Pattern"
|
||||
description="Responsive container with padding and max-width"
|
||||
code={`<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<h1 className="text-3xl font-bold">Page Title</h1>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Content Card</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Your main content goes here.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>`}
|
||||
>
|
||||
<div className="rounded-lg border bg-muted/30 p-2">
|
||||
<div className="container mx-auto px-4 py-8 bg-background rounded">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<h2 className="text-2xl font-bold">Page Title</h2>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Content Card</CardTitle>
|
||||
<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.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<BeforeAfter
|
||||
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',
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
after={{
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ExampleSection>
|
||||
|
||||
{/* 2. Dashboard Grid */}
|
||||
<ExampleSection
|
||||
id="dashboard-grid"
|
||||
title="2. Dashboard Grid"
|
||||
description="Responsive card grid for metrics and data"
|
||||
>
|
||||
<Example
|
||||
title="Responsive Grid Pattern"
|
||||
description="1 → 2 → 3 columns progression with grid"
|
||||
code={`<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{items.map(item => (
|
||||
<Card key={item.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>{item.content}</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>`}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Metric {i}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<BeforeAfter
|
||||
title="Grid vs Flex for Equal Columns"
|
||||
description="Use Grid for equal-width columns, not Flex"
|
||||
before={{
|
||||
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">
|
||||
<div className="text-xs">flex-1</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-[200px] rounded border bg-background p-4">
|
||||
<div className="text-xs">flex-1</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-[200px] rounded border bg-background p-4">
|
||||
<div className="text-xs">flex-1 (odd one out)</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
after={{
|
||||
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">
|
||||
<div className="text-xs">grid-cols</div>
|
||||
</div>
|
||||
<div className="rounded border bg-background p-4">
|
||||
<div className="text-xs">grid-cols</div>
|
||||
</div>
|
||||
<div className="rounded border bg-background p-4">
|
||||
<div className="text-xs">grid-cols (perfect)</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ExampleSection>
|
||||
|
||||
{/* 3. Form Layout */}
|
||||
<ExampleSection
|
||||
id="form-layout"
|
||||
title="3. Form Layout"
|
||||
description="Centered form with appropriate max-width"
|
||||
>
|
||||
<Example
|
||||
title="Centered Form Pattern"
|
||||
description="Form constrained to max-w-md"
|
||||
code={`<div className="container mx-auto px-4 py-8">
|
||||
<Card className="max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Login</CardTitle>
|
||||
<CardDescription>Enter your credentials</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
{/* Form fields */}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>`}
|
||||
>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Card className="max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Login</CardTitle>
|
||||
<CardDescription>Enter your credentials to continue</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Email</div>
|
||||
<div className="h-10 rounded-md border bg-background"></div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Password</div>
|
||||
<div className="h-10 rounded-md border bg-background"></div>
|
||||
</div>
|
||||
<Button className="w-full">Sign In</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* 4. Sidebar Layout */}
|
||||
<ExampleSection
|
||||
id="sidebar-layout"
|
||||
title="4. Sidebar Layout"
|
||||
description="Two-column layout with fixed sidebar"
|
||||
>
|
||||
<Example
|
||||
title="Sidebar + Main Content"
|
||||
description="Grid with fixed sidebar width"
|
||||
code={`<div className="grid lg:grid-cols-[240px_1fr] gap-6">
|
||||
{/* Sidebar */}
|
||||
<aside className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Navigation</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>{/* Nav items */}</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="space-y-4">
|
||||
<Card>{/* Content */}</Card>
|
||||
</main>
|
||||
</div>`}
|
||||
>
|
||||
<div className="grid lg:grid-cols-[240px_1fr] gap-6">
|
||||
{/* Sidebar */}
|
||||
<aside className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Navigation</CardTitle>
|
||||
</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">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Main Content</CardTitle>
|
||||
<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.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* 5. Centered Content */}
|
||||
<ExampleSection
|
||||
id="centered-content"
|
||||
title="5. Centered Content"
|
||||
description="Vertically and horizontally centered layouts"
|
||||
>
|
||||
<Example
|
||||
title="Center with Flexbox"
|
||||
description="Full-height centered content"
|
||||
code={`<div className="flex min-h-screen items-center justify-center">
|
||||
<Card className="max-w-md w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Centered Card</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Content perfectly centered on screen.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>`}
|
||||
>
|
||||
<div className="flex min-h-[400px] items-center justify-center rounded-lg border bg-muted/30 p-4">
|
||||
<Card className="max-w-sm w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Centered Card</CardTitle>
|
||||
<CardDescription>Centered vertically and horizontally</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Perfect for login screens, error pages, and loading states.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Decision Tree */}
|
||||
<ExampleSection
|
||||
id="decision-tree"
|
||||
title="Decision Tree: Grid vs Flex"
|
||||
description="When to use each layout method"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Grid3x3 className="h-5 w-5 text-primary" />
|
||||
<CardTitle>Grid vs Flex Quick Guide</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="default">Use Grid</Badge>
|
||||
<span className="text-sm font-medium">When you need...</span>
|
||||
</div>
|
||||
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||
<li>Equal-width columns (dashboard cards)</li>
|
||||
<li>2D layout (rows AND columns)</li>
|
||||
<li>Consistent grid structure</li>
|
||||
<li>Auto-fill/auto-fit responsive grids</li>
|
||||
</ul>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||
grid grid-cols-3 gap-6
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">Use Flex</Badge>
|
||||
<span className="text-sm font-medium">When you need...</span>
|
||||
</div>
|
||||
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||
<li>Variable-width items (buttons, tags)</li>
|
||||
<li>1D layout (row OR column)</li>
|
||||
<li>Center alignment</li>
|
||||
<li>Space-between/around distribution</li>
|
||||
</ul>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||
flex gap-4 items-center
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Responsive Patterns */}
|
||||
<ExampleSection
|
||||
id="responsive"
|
||||
title="Responsive Patterns"
|
||||
description="Mobile-first breakpoint strategies"
|
||||
>
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">1 → 2 → 3 Progression</CardTitle>
|
||||
<CardDescription>Most common pattern</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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 />
|
||||
Tablet: 2 columns
|
||||
<br />
|
||||
Desktop: 3 columns
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">1 → 2 → 4 Progression</CardTitle>
|
||||
<CardDescription>For smaller cards</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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 />
|
||||
Tablet: 2 columns
|
||||
<br />
|
||||
Desktop: 4 columns
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Stack → Row</CardTitle>
|
||||
<CardDescription>Form buttons, toolbars</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<code className="text-xs">flex flex-col sm:flex-row</code>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Mobile: Stacked vertically
|
||||
<br />
|
||||
Tablet+: Horizontal row
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Hide Sidebar</CardTitle>
|
||||
<CardDescription>Mobile navigation</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<code className="text-xs">hidden lg:block</code>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Mobile: Hidden (use menu)
|
||||
<br />
|
||||
Desktop: Visible sidebar
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ExampleSection>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-16 border-t py-6">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Learn more:{' '}
|
||||
<Link
|
||||
href="/dev/docs/design-system/03-layouts"
|
||||
className="font-medium hover:text-foreground"
|
||||
>
|
||||
Layout Documentation
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
254
frontend/src/app/[locale]/dev/page.tsx
Normal file
254
frontend/src/app/[locale]/dev/page.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* Design System Hub
|
||||
* Central landing page for all interactive design system demonstrations
|
||||
* Access: /dev
|
||||
*/
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
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 { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Design System Hub | FastNext Template',
|
||||
description:
|
||||
'Interactive design system demonstrations with live examples - explore components, layouts, spacing, and forms built with shadcn/ui and Tailwind CSS',
|
||||
};
|
||||
|
||||
const demoPages = [
|
||||
{
|
||||
title: 'Components',
|
||||
description: 'Explore all shadcn/ui components with live examples and copy-paste code',
|
||||
href: '/dev/components',
|
||||
icon: Palette,
|
||||
status: 'enhanced' as const,
|
||||
highlights: ['All variants', 'Interactive demos', 'Copy-paste code'],
|
||||
},
|
||||
{
|
||||
title: 'Layouts',
|
||||
description: 'Essential layout patterns for pages, dashboards, forms, and content',
|
||||
href: '/dev/layouts',
|
||||
icon: Layout,
|
||||
status: 'new' as const,
|
||||
highlights: ['Grid vs Flex', 'Responsive patterns', 'Before/after examples'],
|
||||
},
|
||||
{
|
||||
title: 'Spacing',
|
||||
description: 'Visual demonstrations of spacing philosophy and best practices',
|
||||
href: '/dev/spacing',
|
||||
icon: Ruler,
|
||||
status: 'new' as const,
|
||||
highlights: ['Parent-controlled', 'Gap vs Space-y', 'Anti-patterns'],
|
||||
},
|
||||
{
|
||||
title: 'Forms',
|
||||
description: 'Complete form implementations with validation and error handling',
|
||||
href: '/dev/forms',
|
||||
icon: FileText,
|
||||
status: 'new' as const,
|
||||
highlights: ['react-hook-form', 'Zod validation', 'Loading states'],
|
||||
},
|
||||
];
|
||||
|
||||
const documentationLinks = [
|
||||
{
|
||||
title: 'Quick Start',
|
||||
description: '5-minute crash course',
|
||||
href: '/dev/docs/design-system/00-quick-start',
|
||||
},
|
||||
{
|
||||
title: 'Complete Documentation',
|
||||
description: 'Full design system guide',
|
||||
href: '/dev/docs',
|
||||
},
|
||||
{
|
||||
title: 'AI Guidelines',
|
||||
description: 'Rules for AI code generation',
|
||||
href: '/dev/docs/design-system/08-ai-guidelines',
|
||||
},
|
||||
{
|
||||
title: 'Quick Reference',
|
||||
description: 'Cheat sheet for lookups',
|
||||
href: '/dev/docs/design-system/99-reference',
|
||||
},
|
||||
];
|
||||
|
||||
export default function DesignSystemHub() {
|
||||
return (
|
||||
<div className="bg-background">
|
||||
{/* Hero Section */}
|
||||
<section className="border-b bg-gradient-to-b from-background to-muted/20 py-12">
|
||||
<div className="container mx-auto px-4">
|
||||
<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>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="container mx-auto px-4 py-12">
|
||||
<div className="space-y-12">
|
||||
{/* Demo Pages Grid */}
|
||||
<section className="space-y-6">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{demoPages.map((page) => {
|
||||
const Icon = page.icon;
|
||||
return (
|
||||
<Card
|
||||
key={page.href}
|
||||
className="group relative overflow-hidden transition-all hover:border-primary/50"
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="rounded-lg bg-primary/10 p-2">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
{page.status === 'new' && (
|
||||
<Badge variant="default" className="gap-1">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
New
|
||||
</Badge>
|
||||
)}
|
||||
{page.status === 'enhanced' && <Badge variant="secondary">Enhanced</Badge>}
|
||||
</div>
|
||||
<CardTitle className="mt-4">{page.title}</CardTitle>
|
||||
<CardDescription>{page.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Highlights */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{page.highlights.map((highlight) => (
|
||||
<Badge key={highlight} variant="outline" className="text-xs">
|
||||
{highlight}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<Link href={page.href} className="block">
|
||||
<Button className="w-full gap-2 group-hover:bg-primary/90">
|
||||
Explore {page.title}
|
||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Documentation Links */}
|
||||
<section className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
|
||||
<BookOpen className="h-6 w-6" />
|
||||
Documentation
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Comprehensive guides and reference materials
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Key Features */}
|
||||
<section className="space-y-6">
|
||||
<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>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">🎨 OKLCH Color System</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Perceptually uniform colors with semantic tokens for consistent theming
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">📏 Parent-Controlled Spacing</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Consistent spacing philosophy using gap and space-y utilities
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">♿ WCAG AA Compliant</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Full accessibility support with keyboard navigation and screen readers
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">📱 Mobile-First Responsive</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Tailwind breakpoints with progressive enhancement
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">🤖 AI-Optimized</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Dedicated guidelines for Claude Code, Cursor, and GitHub Copilot
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">🚀 Production-Ready</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Battle-tested patterns with real-world examples
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
502
frontend/src/app/[locale]/dev/spacing/page.tsx
Normal file
502
frontend/src/app/[locale]/dev/spacing/page.tsx
Normal file
@@ -0,0 +1,502 @@
|
||||
/**
|
||||
* Spacing Patterns Demo
|
||||
* Interactive demonstrations of spacing philosophy and best practices
|
||||
* Access: /dev/spacing
|
||||
*/
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
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 { Badge } from '@/components/ui/badge';
|
||||
|
||||
// Code-split heavy dev components
|
||||
const Example = dynamic(
|
||||
() => import('@/components/dev/Example').then((mod) => ({ default: mod.Example })),
|
||||
{ loading: () => <div className="animate-pulse h-32 bg-muted rounded" /> }
|
||||
);
|
||||
|
||||
const ExampleSection = dynamic(
|
||||
() => import('@/components/dev/Example').then((mod) => ({ default: mod.ExampleSection })),
|
||||
{ loading: () => <div className="animate-pulse h-24 bg-muted rounded" /> }
|
||||
);
|
||||
|
||||
const BeforeAfter = dynamic(
|
||||
() => import('@/components/dev/BeforeAfter').then((mod) => ({ default: mod.BeforeAfter })),
|
||||
{ loading: () => <div className="animate-pulse h-48 bg-muted rounded" /> }
|
||||
);
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Spacing Patterns | Dev',
|
||||
description: 'Parent-controlled spacing philosophy and visual demonstrations',
|
||||
};
|
||||
|
||||
export default function SpacingPage() {
|
||||
return (
|
||||
<div className="bg-background">
|
||||
{/* Breadcrumbs */}
|
||||
<DevBreadcrumbs items={[{ label: 'Spacing' }]} />
|
||||
|
||||
{/* Content */}
|
||||
<main className="container mx-auto px-4 py-12">
|
||||
<div className="space-y-12">
|
||||
{/* 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.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">gap</Badge>
|
||||
<Badge variant="outline">space-y</Badge>
|
||||
<Badge variant="outline">space-x</Badge>
|
||||
<Badge variant="destructive">avoid margin</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Spacing Scale */}
|
||||
<ExampleSection
|
||||
id="spacing-scale"
|
||||
title="Spacing Scale"
|
||||
description="Multiples of 4px (Tailwind's base unit)"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Common Spacing Values</CardTitle>
|
||||
<CardDescription>Use consistent spacing values from the scale</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ class: '2', px: '8px', rem: '0.5rem', use: 'Tight (label → input)' },
|
||||
{ class: '4', px: '16px', rem: '1rem', use: 'Standard (form fields)' },
|
||||
{ class: '6', px: '24px', rem: '1.5rem', use: 'Section spacing' },
|
||||
{ class: '8', px: '32px', rem: '2rem', use: 'Large gaps' },
|
||||
{ class: '12', px: '48px', rem: '3rem', use: 'Section dividers' },
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.class}
|
||||
className="grid grid-cols-[80px_80px_100px_1fr] items-center gap-4"
|
||||
>
|
||||
<code className="text-sm font-mono">gap-{item.class}</code>
|
||||
<span className="text-sm text-muted-foreground">{item.px}</span>
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Gap for Flex/Grid */}
|
||||
<ExampleSection
|
||||
id="gap"
|
||||
title="Gap: For Flex and Grid"
|
||||
description="Preferred method for spacing flex and grid children"
|
||||
>
|
||||
<Example
|
||||
title="Gap with Flex"
|
||||
description="Horizontal and vertical spacing"
|
||||
code={`{/* Horizontal */}
|
||||
<div className="flex gap-4">
|
||||
<Button>Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
|
||||
{/* Vertical */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
{/* Cards */}
|
||||
</div>`}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Horizontal */}
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">Horizontal (gap-4)</p>
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vertical */}
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">Vertical (gap-4)</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-lg border bg-muted p-3 text-sm">Item 1</div>
|
||||
<div className="rounded-lg border bg-muted p-3 text-sm">Item 2</div>
|
||||
<div className="rounded-lg border bg-muted p-3 text-sm">Item 3</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div>
|
||||
<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">
|
||||
Card {i}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Space-y for Stacks */}
|
||||
<ExampleSection
|
||||
id="space-y"
|
||||
title="Space-y: For Vertical Stacks"
|
||||
description="Simple vertical spacing without flex/grid"
|
||||
>
|
||||
<Example
|
||||
title="Space-y Pattern"
|
||||
description="Adds margin-top to all children except first"
|
||||
code={`<div className="space-y-4">
|
||||
<div>First item (no margin)</div>
|
||||
<div>Second item (mt-4)</div>
|
||||
<div>Third item (mt-4)</div>
|
||||
</div>
|
||||
|
||||
{/* Form example */}
|
||||
<form className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Email</Label>
|
||||
<Input />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Password</Label>
|
||||
<Input />
|
||||
</div>
|
||||
<Button>Submit</Button>
|
||||
</form>`}
|
||||
>
|
||||
<div className="max-w-md space-y-6">
|
||||
{/* Visual demo */}
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-4">Visual Demo (space-y-4)</p>
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Form example */}
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-4">Form Example (space-y-4)</p>
|
||||
<div className="space-y-4 rounded-lg border p-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Email</div>
|
||||
<div className="h-10 rounded-md border bg-background"></div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Password</div>
|
||||
<div className="h-10 rounded-md border bg-background"></div>
|
||||
</div>
|
||||
<Button className="w-full">Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Anti-pattern: Child Margins */}
|
||||
<ExampleSection
|
||||
id="anti-patterns"
|
||||
title="Anti-patterns to Avoid"
|
||||
description="Common spacing mistakes"
|
||||
>
|
||||
<BeforeAfter
|
||||
title="Don't Let Children Control Spacing"
|
||||
description="Parent should control spacing, not children"
|
||||
before={{
|
||||
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">
|
||||
<div>Child 1</div>
|
||||
<code className="text-[10px] text-destructive">no margin</code>
|
||||
</div>
|
||||
<div className="rounded bg-muted p-2 text-xs">
|
||||
<div>Child 2</div>
|
||||
<code className="text-[10px] text-destructive">mt-4</code>
|
||||
</div>
|
||||
<div className="rounded bg-muted p-2 text-xs">
|
||||
<div>Child 3</div>
|
||||
<code className="text-[10px] text-destructive">mt-4</code>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
after={{
|
||||
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>
|
||||
</div>
|
||||
<div className="rounded bg-muted p-2 text-xs">
|
||||
<div>Child 2</div>
|
||||
<code className="text-[10px] text-green-600">clean, no margin</code>
|
||||
</div>
|
||||
<div className="rounded bg-muted p-2 text-xs">
|
||||
<div>Child 3</div>
|
||||
<code className="text-[10px] text-green-600">clean, no margin</code>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<BeforeAfter
|
||||
title="Use Gap, Not Margin for Buttons"
|
||||
description="Button groups should use gap, not margins"
|
||||
before={{
|
||||
caption: 'Margin on children - harder to maintain',
|
||||
content: (
|
||||
<div className="flex rounded-lg border p-4">
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" className="ml-4">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
after={{
|
||||
caption: 'Gap on parent - clean and flexible',
|
||||
content: (
|
||||
<div className="flex gap-4 rounded-lg border p-4">
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Save</Button>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Decision Tree */}
|
||||
<ExampleSection
|
||||
id="decision-tree"
|
||||
title="Decision Tree: Which Spacing Method?"
|
||||
description="Choose the right spacing utility"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Ruler className="h-5 w-5 text-primary" />
|
||||
<CardTitle>Spacing Decision Tree</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
{/* Gap */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="default">Use gap</Badge>
|
||||
<span className="text-sm font-medium">When...</span>
|
||||
</div>
|
||||
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||
<li>Parent is flex or grid</li>
|
||||
<li>All children need equal spacing</li>
|
||||
<li>Responsive spacing (gap-4 md:gap-6)</li>
|
||||
</ul>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||
flex gap-4
|
||||
<br />
|
||||
grid grid-cols-3 gap-6
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Space-y */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">Use space-y</Badge>
|
||||
<span className="text-sm font-medium">When...</span>
|
||||
</div>
|
||||
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||
<li>Vertical stack without flex/grid</li>
|
||||
<li>Form fields</li>
|
||||
<li>Content sections</li>
|
||||
</ul>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||
space-y-4
|
||||
<br />
|
||||
space-y-6
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Margin */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="destructive">Use margin</Badge>
|
||||
<span className="text-sm font-medium">Only when...</span>
|
||||
</div>
|
||||
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||
<li>Exception case (one child needs different spacing)</li>
|
||||
<li>Negative margin for overlap effects</li>
|
||||
<li>Cannot modify parent (external component)</li>
|
||||
</ul>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||
mt-8 {/* exception */}
|
||||
<br />
|
||||
-mt-4 {/* overlap */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Padding */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">Use padding</Badge>
|
||||
<span className="text-sm font-medium">When...</span>
|
||||
</div>
|
||||
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||
<li>Internal spacing within a component</li>
|
||||
<li>Card/container padding</li>
|
||||
<li>Button padding</li>
|
||||
</ul>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||
p-4 {/* all sides */}
|
||||
<br />
|
||||
px-4 py-2 {/* horizontal & vertical */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Common Patterns */}
|
||||
<ExampleSection
|
||||
id="common-patterns"
|
||||
title="Common Patterns"
|
||||
description="Real-world spacing examples"
|
||||
>
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Form Fields</CardTitle>
|
||||
<CardDescription>Parent: space-y-4, Field: space-y-2</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<code className="text-xs block mb-3">
|
||||
{`<form className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Email</Label>
|
||||
<Input />
|
||||
</div>
|
||||
</form>`}
|
||||
</code>
|
||||
<div className="space-y-4 rounded-lg border bg-muted/30 p-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-medium">Email</div>
|
||||
<div className="h-8 rounded border bg-background"></div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-medium">Password</div>
|
||||
<div className="h-8 rounded border bg-background"></div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Button Group</CardTitle>
|
||||
<CardDescription>Use flex gap-4</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<code className="text-xs block mb-3">
|
||||
{`<div className="flex gap-4">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>`}
|
||||
</code>
|
||||
<div className="flex gap-4 rounded-lg border bg-muted/30 p-4">
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Save</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Card Grid</CardTitle>
|
||||
<CardDescription>Use grid with gap-6</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<code className="text-xs block mb-3">
|
||||
{`<div className="grid grid-cols-2 gap-6">
|
||||
<Card>...</Card>
|
||||
</div>`}
|
||||
</code>
|
||||
<div className="grid grid-cols-2 gap-4 rounded-lg border bg-muted/30 p-4">
|
||||
<div className="h-16 rounded border bg-background"></div>
|
||||
<div className="h-16 rounded border bg-background"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Content Stack</CardTitle>
|
||||
<CardDescription>Use space-y-6 for sections</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<code className="text-xs block mb-3">
|
||||
{`<div className="space-y-6">
|
||||
<section>...</section>
|
||||
<section>...</section>
|
||||
</div>`}
|
||||
</code>
|
||||
<div className="space-y-4 rounded-lg border bg-muted/30 p-4">
|
||||
<div className="h-12 rounded border bg-background"></div>
|
||||
<div className="h-12 rounded border bg-background"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ExampleSection>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-16 border-t py-6">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Learn more:{' '}
|
||||
<Link
|
||||
href="/dev/docs/design-system/04-spacing-philosophy"
|
||||
className="font-medium hover:text-foreground"
|
||||
>
|
||||
Spacing Philosophy Documentation
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
frontend/src/app/[locale]/forbidden/page.tsx
Normal file
47
frontend/src/app/[locale]/forbidden/page.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 403 Forbidden Page
|
||||
* Displayed when users try to access resources they don't have permission for
|
||||
*/
|
||||
|
||||
/* istanbul ignore next - Next.js type import for metadata */
|
||||
import type { Metadata } from 'next';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import { ShieldAlert } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export const metadata: Metadata = /* istanbul ignore next */ {
|
||||
title: '403 - Forbidden',
|
||||
description: 'You do not have permission to access this resource',
|
||||
};
|
||||
|
||||
export default function ForbiddenPage() {
|
||||
return (
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
<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.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button asChild variant="default">
|
||||
<Link href="/dashboard">Go to Dashboard</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/">Go to Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
88
frontend/src/app/[locale]/layout.tsx
Normal file
88
frontend/src/app/[locale]/layout.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Geist, Geist_Mono } from 'next/font/google';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { routing } from '@/lib/i18n/routing';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages } from 'next-intl/server';
|
||||
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
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
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',
|
||||
};
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
}>) {
|
||||
// Await params in Next.js 15
|
||||
const { locale } = await params;
|
||||
|
||||
// Ensure that the incoming `locale` is valid
|
||||
if (!routing.locales.includes(locale as 'en' | 'it')) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Providing all messages to the client
|
||||
// side is the easiest way to get started
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<head>
|
||||
{/* Theme initialization script - runs before React hydrates to prevent FOUC */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
try {
|
||||
const theme = localStorage.getItem('theme') || 'system';
|
||||
let resolved;
|
||||
|
||||
if (theme === 'system') {
|
||||
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
} else {
|
||||
resolved = theme;
|
||||
}
|
||||
|
||||
document.documentElement.classList.remove('light', 'dark');
|
||||
document.documentElement.classList.add(resolved);
|
||||
} catch (e) {
|
||||
// Silently fail - theme will be set by ThemeProvider
|
||||
}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<AuthProvider>
|
||||
<AuthInitializer />
|
||||
<Providers>{children}</Providers>
|
||||
</AuthProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
99
frontend/src/app/[locale]/page.tsx
Executable file
99
frontend/src/app/[locale]/page.tsx
Executable file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Homepage / Landing Page
|
||||
* Main landing page for the FastNext Template project
|
||||
* Showcases features, tech stack, and provides demos for developers
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import { Header } from '@/components/home/Header';
|
||||
import { HeroSection } from '@/components/home/HeroSection';
|
||||
import { ContextSection } from '@/components/home/ContextSection';
|
||||
import { AnimatedTerminal } from '@/components/home/AnimatedTerminal';
|
||||
import { FeatureGrid } from '@/components/home/FeatureGrid';
|
||||
import { DemoSection } from '@/components/home/DemoSection';
|
||||
import { StatsSection } from '@/components/home/StatsSection';
|
||||
import { TechStackSection } from '@/components/home/TechStackSection';
|
||||
import { PhilosophySection } from '@/components/home/PhilosophySection';
|
||||
import { QuickStartCode } from '@/components/home/QuickStartCode';
|
||||
import { CTASection } from '@/components/home/CTASection';
|
||||
import { DemoCredentialsModal } from '@/components/home/DemoCredentialsModal';
|
||||
|
||||
export default function Home() {
|
||||
const [demoModalOpen, setDemoModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Header Navigation */}
|
||||
<Header onOpenDemoModal={() => setDemoModalOpen(true)} />
|
||||
|
||||
{/* Main Content */}
|
||||
<main>
|
||||
{/* Hero Section with CTAs */}
|
||||
<HeroSection onOpenDemoModal={() => setDemoModalOpen(true)} />
|
||||
|
||||
{/* What is this template? */}
|
||||
<ContextSection />
|
||||
|
||||
{/* Animated Terminal with Quick Start */}
|
||||
<AnimatedTerminal />
|
||||
|
||||
{/* 6 Feature Cards Grid */}
|
||||
<FeatureGrid />
|
||||
|
||||
{/* Interactive Demo Cards */}
|
||||
<DemoSection />
|
||||
|
||||
{/* Statistics with Animated Counters */}
|
||||
<StatsSection />
|
||||
|
||||
{/* Tech Stack Grid */}
|
||||
<TechStackSection />
|
||||
|
||||
{/* For Developers, By Developers */}
|
||||
<PhilosophySection />
|
||||
|
||||
{/* Quick Start Code Block */}
|
||||
<QuickStartCode />
|
||||
|
||||
{/* Final CTA Section */}
|
||||
<CTASection onOpenDemoModal={() => setDemoModalOpen(true)} />
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t bg-muted/30">
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} FastNext Template. MIT Licensed.
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-sm text-muted-foreground">
|
||||
<Link href="/demos" className="hover:text-foreground transition-colors">
|
||||
Demo Tour
|
||||
</Link>
|
||||
<Link href="/dev" className="hover:text-foreground transition-colors">
|
||||
Design System
|
||||
</Link>
|
||||
<a
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<Link href="/dev/docs" className="hover:text-foreground transition-colors">
|
||||
Documentation
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Shared Demo Credentials Modal */}
|
||||
<DemoCredentialsModal open={demoModalOpen} onClose={() => setDemoModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user