auth rework
This commit is contained in:
44
apps/website/components/ui/FormField.tsx
Normal file
44
apps/website/components/ui/FormField.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface FormFieldProps {
|
||||
label: string;
|
||||
icon?: React.ElementType;
|
||||
children: React.ReactNode;
|
||||
required?: boolean;
|
||||
error?: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form field wrapper with label, optional icon, required indicator, and error/hint display.
|
||||
* Used for consistent form field layout throughout the app.
|
||||
*/
|
||||
export default function FormField({
|
||||
label,
|
||||
icon: Icon,
|
||||
children,
|
||||
required = false,
|
||||
error,
|
||||
hint,
|
||||
}: FormFieldProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-300">
|
||||
<div className="flex items-center gap-2">
|
||||
{Icon && <Icon className="w-4 h-4 text-gray-500" />}
|
||||
{label}
|
||||
{required && <span className="text-racing-red">*</span>}
|
||||
</div>
|
||||
</label>
|
||||
{children}
|
||||
{error && (
|
||||
<p className="text-xs text-racing-red mt-1">{error}</p>
|
||||
)}
|
||||
{hint && !error && (
|
||||
<p className="text-xs text-gray-500 mt-1">{hint}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
apps/website/components/ui/InfoBanner.tsx
Normal file
71
apps/website/components/ui/InfoBanner.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Info, AlertTriangle, CheckCircle, XCircle } from 'lucide-react';
|
||||
|
||||
type BannerType = 'info' | 'warning' | 'success' | 'error';
|
||||
|
||||
interface InfoBannerProps {
|
||||
type?: BannerType;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
icon?: React.ElementType;
|
||||
}
|
||||
|
||||
const bannerConfig: Record<BannerType, {
|
||||
icon: React.ElementType;
|
||||
bg: string;
|
||||
border: string;
|
||||
titleColor: string;
|
||||
}> = {
|
||||
info: {
|
||||
icon: Info,
|
||||
bg: 'bg-iron-gray/30',
|
||||
border: 'border-charcoal-outline/50',
|
||||
titleColor: 'text-gray-300',
|
||||
},
|
||||
warning: {
|
||||
icon: AlertTriangle,
|
||||
bg: 'bg-warning-amber/10',
|
||||
border: 'border-warning-amber/30',
|
||||
titleColor: 'text-warning-amber',
|
||||
},
|
||||
success: {
|
||||
icon: CheckCircle,
|
||||
bg: 'bg-performance-green/10',
|
||||
border: 'border-performance-green/30',
|
||||
titleColor: 'text-performance-green',
|
||||
},
|
||||
error: {
|
||||
icon: XCircle,
|
||||
bg: 'bg-racing-red/10',
|
||||
border: 'border-racing-red/30',
|
||||
titleColor: 'text-racing-red',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Info banner component for displaying contextual information, warnings, or notices.
|
||||
* Used throughout the app for important messages and helper text.
|
||||
*/
|
||||
export default function InfoBanner({
|
||||
type = 'info',
|
||||
title,
|
||||
children,
|
||||
icon: CustomIcon,
|
||||
}: InfoBannerProps) {
|
||||
const config = bannerConfig[type];
|
||||
const Icon = CustomIcon || config.icon;
|
||||
|
||||
return (
|
||||
<div className={`flex items-start gap-3 p-4 rounded-lg border ${config.bg} ${config.border}`}>
|
||||
<Icon className="w-5 h-5 text-gray-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-gray-400">
|
||||
{title && (
|
||||
<p className={`font-medium mb-1 ${config.titleColor}`}>{title}</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
apps/website/components/ui/PageHeader.tsx
Normal file
44
apps/website/components/ui/PageHeader.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface PageHeaderProps {
|
||||
icon: React.ElementType;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: React.ReactNode;
|
||||
iconGradient?: string;
|
||||
iconBorder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page header component with icon, title, description, and optional action.
|
||||
* Used at the top of pages for consistent page titling.
|
||||
*/
|
||||
export default function PageHeader({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
iconGradient = 'from-iron-gray to-deep-graphite',
|
||||
iconBorder = 'border-charcoal-outline',
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white flex items-center gap-4">
|
||||
<div className={`p-3 rounded-xl bg-gradient-to-br ${iconGradient} border ${iconBorder}`}>
|
||||
<Icon className="w-7 h-7 text-gray-300" />
|
||||
</div>
|
||||
{title}
|
||||
</h1>
|
||||
{description && (
|
||||
<p className="text-gray-400 mt-2 ml-16">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
apps/website/components/ui/SectionHeader.tsx
Normal file
40
apps/website/components/ui/SectionHeader.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface SectionHeaderProps {
|
||||
icon: React.ElementType;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: React.ReactNode;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Section header component with icon, title, optional description and action.
|
||||
* Used at the top of card sections throughout the app.
|
||||
*/
|
||||
export default function SectionHeader({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
color = 'text-primary-blue'
|
||||
}: SectionHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-5 border-b border-charcoal-outline bg-gradient-to-r from-iron-gray/30 to-transparent">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg bg-iron-gray/50 ${color}`}>
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
{title}
|
||||
</h2>
|
||||
{description && (
|
||||
<p className="text-sm text-gray-500 mt-1 ml-12">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
apps/website/components/ui/StatCard.tsx
Normal file
56
apps/website/components/ui/StatCard.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Card from './Card';
|
||||
|
||||
interface StatCardProps {
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
value: string;
|
||||
subValue?: string;
|
||||
color?: string;
|
||||
bgColor?: string;
|
||||
trend?: {
|
||||
value: number;
|
||||
isPositive: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistics card component for displaying metrics with icon, label, value, and optional trend.
|
||||
* Used in dashboards and overview sections.
|
||||
*/
|
||||
export default function StatCard({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
subValue,
|
||||
color = 'text-primary-blue',
|
||||
bgColor = 'bg-primary-blue/10',
|
||||
trend,
|
||||
}: StatCardProps) {
|
||||
return (
|
||||
<Card className="p-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className={`p-2 rounded-lg ${bgColor}`}>
|
||||
<Icon className={`w-4 h-4 ${color}`} />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">{label}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-white">{value}</div>
|
||||
{subValue && (
|
||||
<div className="text-xs text-gray-500 mt-1">{subValue}</div>
|
||||
)}
|
||||
</div>
|
||||
{trend && (
|
||||
<div className={`flex items-center gap-1 text-sm ${trend.isPositive ? 'text-performance-green' : 'text-racing-red'}`}>
|
||||
<span>{trend.isPositive ? '↑' : '↓'}</span>
|
||||
<span>{Math.abs(trend.value)}%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
68
apps/website/components/ui/StatusBadge.tsx
Normal file
68
apps/website/components/ui/StatusBadge.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type StatusType = 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'pending';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: StatusType;
|
||||
label: string;
|
||||
icon?: React.ElementType;
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
const statusConfig: Record<StatusType, { color: string; bg: string; border: string }> = {
|
||||
success: {
|
||||
color: 'text-performance-green',
|
||||
bg: 'bg-performance-green/10',
|
||||
border: 'border-performance-green/30',
|
||||
},
|
||||
warning: {
|
||||
color: 'text-warning-amber',
|
||||
bg: 'bg-warning-amber/10',
|
||||
border: 'border-warning-amber/30',
|
||||
},
|
||||
error: {
|
||||
color: 'text-racing-red',
|
||||
bg: 'bg-racing-red/10',
|
||||
border: 'border-racing-red/30',
|
||||
},
|
||||
info: {
|
||||
color: 'text-primary-blue',
|
||||
bg: 'bg-primary-blue/10',
|
||||
border: 'border-primary-blue/30',
|
||||
},
|
||||
neutral: {
|
||||
color: 'text-gray-400',
|
||||
bg: 'bg-iron-gray',
|
||||
border: 'border-charcoal-outline',
|
||||
},
|
||||
pending: {
|
||||
color: 'text-warning-amber',
|
||||
bg: 'bg-warning-amber/10',
|
||||
border: 'border-warning-amber/30',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Status badge component for displaying status indicators.
|
||||
* Used for showing status of items like invoices, sponsorships, etc.
|
||||
*/
|
||||
export default function StatusBadge({
|
||||
status,
|
||||
label,
|
||||
icon: Icon,
|
||||
size = 'sm',
|
||||
}: StatusBadgeProps) {
|
||||
const config = statusConfig[status];
|
||||
const sizeClasses = size === 'sm'
|
||||
? 'px-2 py-0.5 text-xs'
|
||||
: 'px-3 py-1 text-sm';
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1.5 rounded-full font-medium border ${config.bg} ${config.color} ${config.border} ${sizeClasses}`}>
|
||||
{Icon && <Icon className={size === 'sm' ? 'w-3 h-3' : 'w-4 h-4'} />}
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
76
apps/website/components/ui/Toggle.tsx
Normal file
76
apps/website/components/ui/Toggle.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
|
||||
interface ToggleProps {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
label: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle switch component with Framer Motion animation.
|
||||
* Used for boolean settings/preferences.
|
||||
*/
|
||||
export default function Toggle({
|
||||
checked,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
disabled = false,
|
||||
}: ToggleProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<label className={`flex items-start justify-between cursor-pointer py-3 border-b border-charcoal-outline/50 last:border-b-0 ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}>
|
||||
<div className="flex-1 pr-4">
|
||||
<span className="text-gray-200 font-medium">{label}</span>
|
||||
{description && (
|
||||
<p className="text-sm text-gray-500 mt-0.5">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
onClick={() => !disabled && onChange(!checked)}
|
||||
disabled={disabled}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors duration-200 flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-primary-blue/50 ${
|
||||
checked
|
||||
? 'bg-primary-blue'
|
||||
: 'bg-iron-gray'
|
||||
} ${disabled ? 'cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{/* Glow effect when active */}
|
||||
{checked && (
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full bg-primary-blue"
|
||||
initial={{ boxShadow: '0 0 0px rgba(25, 140, 255, 0)' }}
|
||||
animate={{ boxShadow: '0 0 12px rgba(25, 140, 255, 0.4)' }}
|
||||
transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Knob */}
|
||||
<motion.span
|
||||
className="absolute top-0.5 w-5 h-5 bg-white rounded-full shadow-md"
|
||||
initial={false}
|
||||
animate={{
|
||||
x: checked ? 24 : 2,
|
||||
scale: 1,
|
||||
}}
|
||||
whileTap={{ scale: disabled ? 1 : 0.9 }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
duration: shouldReduceMotion ? 0 : undefined,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user