website refactor

This commit is contained in:
2026-01-17 15:46:55 +01:00
parent 4d5ce9bfd6
commit 72a626ce71
346 changed files with 19308 additions and 8605 deletions

View File

@@ -0,0 +1,44 @@
'use client';
import React from 'react';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
interface AdminDangerZonePanelProps {
title: string;
description: string;
children: React.ReactNode;
}
/**
* AdminDangerZonePanel
*
* Semantic panel for destructive or dangerous admin actions.
* Restrained but clear warning styling.
*/
export function AdminDangerZonePanel({
title,
description,
children
}: AdminDangerZonePanelProps) {
return (
<Card borderColor="border-error-red/30" bg="bg-error-red/5">
<Stack direction={{ base: 'col', md: 'row' }} align="center" justify="between" gap={6}>
<Box>
<Heading level={4} weight="bold" color="text-error-red">
{title}
</Heading>
<Text size="sm" color="text-gray-400" block mt={1}>
{description}
</Text>
</Box>
<Box>
{children}
</Box>
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,32 @@
'use client';
import React from 'react';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
interface AdminDataTableProps {
children: React.ReactNode;
maxHeight?: string | number;
}
/**
* AdminDataTable
*
* Semantic wrapper for high-density admin tables.
* Provides a consistent container with "Precision Racing Minimal" styling.
*/
export function AdminDataTable({
children,
maxHeight
}: AdminDataTableProps) {
return (
<Card p={0} overflow="hidden">
<Box
overflow="auto"
maxHeight={maxHeight}
>
{children}
</Box>
</Card>
);
}

View File

@@ -0,0 +1,49 @@
'use client';
import React from 'react';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { LucideIcon } from 'lucide-react';
interface AdminEmptyStateProps {
icon: LucideIcon;
title: string;
description?: string;
action?: React.ReactNode;
}
/**
* AdminEmptyState
*
* Semantic empty state for admin lists and tables.
* Follows "Precision Racing Minimal" theme.
*/
export function AdminEmptyState({
icon,
title,
description,
action
}: AdminEmptyStateProps) {
return (
<Stack center py={20} gap={4}>
<Icon icon={icon} size={12} color="#23272B" />
<Box textAlign="center">
<Text size="lg" weight="bold" color="text-white" block>
{title}
</Text>
{description && (
<Text size="sm" color="text-gray-500" block mt={1}>
{description}
</Text>
)}
</Box>
{action && (
<Box mt={2}>
{action}
</Box>
)}
</Stack>
);
}

View File

@@ -0,0 +1,53 @@
'use client';
import React from 'react';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { ProgressLine } from '@/components/shared/ux/ProgressLine';
interface AdminHeaderPanelProps {
title: string;
description?: string;
actions?: React.ReactNode;
isLoading?: boolean;
}
/**
* AdminHeaderPanel
*
* Semantic header for admin pages.
* Includes title, description, actions, and a progress line for loading states.
*/
export function AdminHeaderPanel({
title,
description,
actions,
isLoading = false
}: AdminHeaderPanelProps) {
return (
<Box position="relative" pb={4} borderBottom borderColor="border-charcoal-outline">
<Stack direction="row" align="center" justify="between">
<Box>
<Heading level={1} weight="bold" color="text-white">
{title}
</Heading>
{description && (
<Text size="sm" color="text-gray-400" block mt={1}>
{description}
</Text>
)}
</Box>
{actions && (
<Stack direction="row" align="center" gap={3}>
{actions}
</Stack>
)}
</Stack>
<Box position="absolute" bottom="0" left="0" w="full">
<ProgressLine isLoading={isLoading} />
</Box>
</Box>
);
}

View File

@@ -0,0 +1,45 @@
'use client';
import React from 'react';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
interface AdminSectionHeaderProps {
title: string;
description?: string;
actions?: React.ReactNode;
}
/**
* AdminSectionHeader
*
* Semantic header for sections within admin pages.
* Follows "Precision Racing Minimal" theme: dense, clear hierarchy.
*/
export function AdminSectionHeader({
title,
description,
actions
}: AdminSectionHeaderProps) {
return (
<Stack direction="row" align="center" justify="between" mb={4}>
<Box>
<Heading level={3} weight="bold" color="text-white">
{title}
</Heading>
{description && (
<Text size="xs" color="text-gray-500" block mt={0.5}>
{description}
</Text>
)}
</Box>
{actions && (
<Stack direction="row" align="center" gap={2}>
{actions}
</Stack>
)}
</Stack>
);
}

View File

@@ -0,0 +1,45 @@
'use client';
import React from 'react';
import { Grid } from '@/ui/Grid';
import { StatCard } from '@/ui/StatCard';
import { LucideIcon } from 'lucide-react';
interface AdminStat {
label: string;
value: string | number;
icon: LucideIcon;
variant?: 'blue' | 'purple' | 'green' | 'orange';
trend?: {
value: number;
isPositive: boolean;
};
}
interface AdminStatsPanelProps {
stats: AdminStat[];
}
/**
* AdminStatsPanel
*
* Semantic container for admin statistics.
* Renders a grid of StatCards.
*/
export function AdminStatsPanel({ stats }: AdminStatsPanelProps) {
return (
<Grid cols={1} mdCols={2} lgCols={4} gap={4}>
{stats.map((stat, index) => (
<StatCard
key={stat.label}
label={stat.label}
value={stat.value}
icon={stat.icon}
variant={stat.variant}
trend={stat.trend}
delay={index * 0.05}
/>
))}
</Grid>
);
}

View File

@@ -0,0 +1,37 @@
'use client';
import React from 'react';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
interface AdminToolbarProps {
children: React.ReactNode;
leftContent?: React.ReactNode;
}
/**
* AdminToolbar
*
* Semantic toolbar for admin pages.
* Used for filters, search, and secondary actions.
*/
export function AdminToolbar({
children,
leftContent
}: AdminToolbarProps) {
return (
<Card p={3} bg="bg-charcoal/50" borderColor="border-charcoal-outline">
<Stack direction="row" align="center" justify="between" gap={4} wrap>
{leftContent && (
<Box flexGrow={1}>
{leftContent}
</Box>
)}
<Stack direction="row" align="center" gap={3} flexGrow={leftContent ? 0 : 1} wrap>
{children}
</Stack>
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,170 @@
'use client';
import React from 'react';
import {
Table,
TableHead,
TableBody,
TableRow,
TableHeader,
TableCell
} from '@/ui/Table';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { SimpleCheckbox } from '@/ui/SimpleCheckbox';
import { UserStatusTag } from './UserStatusTag';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { Shield, Trash2, MoreVertical } from 'lucide-react';
import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
interface AdminUsersTableProps {
users: AdminUsersViewData['users'];
selectedUserIds: string[];
onSelectUser: (userId: string) => void;
onSelectAll: () => void;
onUpdateStatus: (userId: string, status: string) => void;
onDeleteUser: (userId: string) => void;
deletingUserId?: string | null;
}
/**
* AdminUsersTable
*
* Semantic table for managing users.
* High-density, instrument-grade UI.
*/
export function AdminUsersTable({
users,
selectedUserIds,
onSelectUser,
onSelectAll,
onUpdateStatus,
onDeleteUser,
deletingUserId
}: AdminUsersTableProps) {
const allSelected = users.length > 0 && selectedUserIds.length === users.length;
return (
<Table>
<TableHead>
<TableRow>
<TableHeader width="10">
<SimpleCheckbox
checked={allSelected}
onChange={onSelectAll}
aria-label="Select all users"
/>
</TableHeader>
<TableHeader>User</TableHeader>
<TableHeader>Roles</TableHeader>
<TableHeader>Status</TableHeader>
<TableHeader>Last Login</TableHeader>
<TableHeader textAlign="right">Actions</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => (
<TableRow key={user.id} variant={selectedUserIds.includes(user.id) ? 'highlight' : 'default'}>
<TableCell>
<SimpleCheckbox
checked={selectedUserIds.includes(user.id)}
onChange={() => onSelectUser(user.id)}
aria-label={`Select user ${user.displayName}`}
/>
</TableCell>
<TableCell>
<Stack direction="row" align="center" gap={3}>
<Box
bg="bg-primary-blue/10"
rounded="full"
p={2}
border
borderColor="border-primary-blue/20"
>
<Icon icon={Shield} size={4} color="#198CFF" />
</Box>
<Box>
<Text weight="semibold" color="text-white" block>
{user.displayName}
</Text>
<Text size="xs" color="text-gray-500" block>
{user.email}
</Text>
</Box>
</Stack>
</TableCell>
<TableCell>
<Stack direction="row" gap={1.5} wrap>
{user.roles.map((role) => (
<Box
key={role}
px={2}
py={0.5}
rounded="full"
bg="bg-charcoal-outline/30"
border
borderColor="border-charcoal-outline"
>
<Text size="xs" weight="medium" color="text-gray-300">
{role}
</Text>
</Box>
))}
</Stack>
</TableCell>
<TableCell>
<UserStatusTag status={user.status} />
</TableCell>
<TableCell>
<Text size="sm" color="text-gray-400">
{user.lastLoginAt ? DateDisplay.formatShort(user.lastLoginAt) : 'Never'}
</Text>
</TableCell>
<TableCell>
<Stack direction="row" align="center" justify="end" gap={2}>
{user.status === 'active' ? (
<Button
size="sm"
variant="secondary"
onClick={() => onUpdateStatus(user.id, 'suspended')}
>
Suspend
</Button>
) : user.status === 'suspended' ? (
<Button
size="sm"
variant="secondary"
onClick={() => onUpdateStatus(user.id, 'active')}
>
Activate
</Button>
) : null}
<Button
size="sm"
variant="secondary"
onClick={() => onDeleteUser(user.id)}
disabled={deletingUserId === user.id}
icon={<Icon icon={Trash2} size={3} />}
>
{deletingUserId === user.id ? '...' : ''}
</Button>
<Button
size="sm"
variant="ghost"
icon={<Icon icon={MoreVertical} size={4} />}
>
{''}
</Button>
</Stack>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,93 @@
'use client';
import React from 'react';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { motion, AnimatePresence } from 'framer-motion';
interface BulkActionBarProps {
selectedCount: number;
actions: {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
icon?: React.ReactNode;
}[];
onClearSelection: () => void;
}
/**
* BulkActionBar
*
* Floating action bar that appears when items are selected in a table.
*/
export function BulkActionBar({
selectedCount,
actions,
onClearSelection
}: BulkActionBarProps) {
return (
<AnimatePresence>
{selectedCount > 0 && (
<Box
as={motion.div}
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
position="fixed"
bottom="8"
left="1/2"
translateX="-1/2"
zIndex={50}
bg="bg-surface-charcoal"
border
borderColor="border-primary-blue/50"
rounded="xl"
shadow="xl"
px={6}
py={4}
bgOpacity={0.9}
blur="md"
>
<Stack direction="row" align="center" gap={8}>
<Stack direction="row" align="center" gap={3}>
<Box bg="bg-primary-blue" rounded="full" px={2} py={0.5}>
<Text size="xs" weight="bold" color="text-white">
{selectedCount}
</Text>
</Box>
<Text size="sm" weight="medium" color="text-white">
Items Selected
</Text>
</Stack>
<Box w="px" h="6" bg="bg-charcoal-outline" />
<Stack direction="row" align="center" gap={3}>
{actions.map((action) => (
<Button
key={action.label}
size="sm"
variant={action.variant === 'danger' ? 'secondary' : (action.variant || 'primary')}
onClick={action.onClick}
icon={action.icon}
>
{action.label}
</Button>
))}
<Button
size="sm"
variant="ghost"
onClick={onClearSelection}
>
Cancel
</Button>
</Stack>
</Stack>
</Box>
)}
</AnimatePresence>
);
}

View File

@@ -2,14 +2,13 @@
import React from 'react';
import { Filter, Search } from 'lucide-react';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { Grid } from '@/ui/Grid';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Select } from '@/ui/Select';
import { AdminToolbar } from './AdminToolbar';
interface UserFiltersProps {
search: string;
@@ -31,13 +30,11 @@ export function UserFilters({
onClearFilters,
}: UserFiltersProps) {
return (
<Card>
<Stack gap={4}>
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={2}>
<Icon icon={Filter} size={4} color="#9ca3af" />
<Text weight="medium" color="text-white">Filters</Text>
</Stack>
<AdminToolbar
leftContent={
<Stack direction="row" align="center" gap={2}>
<Icon icon={Filter} size={4} color="#9ca3af" />
<Text weight="medium" color="text-white">Filters</Text>
{(search || roleFilter || statusFilter) && (
<Button
onClick={onClearFilters}
@@ -48,39 +45,38 @@ export function UserFilters({
</Button>
)}
</Stack>
<Grid cols={3} gap={4}>
<Input
type="text"
placeholder="Search by email or name..."
value={search}
onChange={(e) => onSearch(e.target.value)}
icon={<Icon icon={Search} size={4} color="#9ca3af" />}
/>
}
>
<Input
type="text"
placeholder="Search by email or name..."
value={search}
onChange={(e) => onSearch(e.target.value)}
icon={<Icon icon={Search} size={4} color="#9ca3af" />}
width="300px"
/>
<Select
value={roleFilter}
onChange={(e) => onFilterRole(e.target.value)}
options={[
{ value: '', label: 'All Roles' },
{ value: 'owner', label: 'Owner' },
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' },
]}
/>
<Select
value={roleFilter}
onChange={(e) => onFilterRole(e.target.value)}
options={[
{ value: '', label: 'All Roles' },
{ value: 'owner', label: 'Owner' },
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' },
]}
/>
<Select
value={statusFilter}
onChange={(e) => onFilterStatus(e.target.value)}
options={[
{ value: '', label: 'All Status' },
{ value: 'active', label: 'Active' },
{ value: 'suspended', label: 'Suspended' },
{ value: 'deleted', label: 'Deleted' },
]}
/>
</Grid>
</Stack>
</Card>
<Select
value={statusFilter}
onChange={(e) => onFilterStatus(e.target.value)}
options={[
{ value: '', label: 'All Status' },
{ value: 'active', label: 'Active' },
{ value: 'suspended', label: 'Suspended' },
{ value: 'deleted', label: 'Deleted' },
]}
/>
</AdminToolbar>
);
}

View File

@@ -0,0 +1,68 @@
'use client';
import React from 'react';
import { StatusBadge } from '@/ui/StatusBadge';
import {
CheckCircle2,
AlertTriangle,
XCircle,
Clock,
LucideIcon
} from 'lucide-react';
export type UserStatus = 'active' | 'suspended' | 'deleted' | 'pending';
interface UserStatusTagProps {
status: UserStatus | string;
}
interface StatusConfig {
variant: 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'pending';
icon: LucideIcon;
label: string;
}
/**
* UserStatusTag
*
* Semantic status indicator for users.
* Maps status strings to appropriate visual variants and icons.
*/
export function UserStatusTag({ status }: UserStatusTagProps) {
const normalizedStatus = status.toLowerCase() as UserStatus;
const config: Record<UserStatus, StatusConfig> = {
active: {
variant: 'success',
icon: CheckCircle2,
label: 'Active'
},
suspended: {
variant: 'warning',
icon: AlertTriangle,
label: 'Suspended'
},
deleted: {
variant: 'error',
icon: XCircle,
label: 'Deleted'
},
pending: {
variant: 'pending',
icon: Clock,
label: 'Pending'
}
};
const { variant, icon, label } = config[normalizedStatus] || {
variant: 'neutral',
icon: Clock,
label: status
};
return (
<StatusBadge variant={variant} icon={icon}>
{label}
</StatusBadge>
);
}