auth rework

This commit is contained in:
2025-12-17 15:34:56 +01:00
parent a213a5cf9f
commit 75eaa1aa9f
24 changed files with 6115 additions and 1992 deletions

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}