website refactor
This commit is contained in:
44
apps/website/components/admin/AdminDangerZonePanel.tsx
Normal file
44
apps/website/components/admin/AdminDangerZonePanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
32
apps/website/components/admin/AdminDataTable.tsx
Normal file
32
apps/website/components/admin/AdminDataTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
apps/website/components/admin/AdminEmptyState.tsx
Normal file
49
apps/website/components/admin/AdminEmptyState.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
53
apps/website/components/admin/AdminHeaderPanel.tsx
Normal file
53
apps/website/components/admin/AdminHeaderPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
apps/website/components/admin/AdminSectionHeader.tsx
Normal file
45
apps/website/components/admin/AdminSectionHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
apps/website/components/admin/AdminStatsPanel.tsx
Normal file
45
apps/website/components/admin/AdminStatsPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
37
apps/website/components/admin/AdminToolbar.tsx
Normal file
37
apps/website/components/admin/AdminToolbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
170
apps/website/components/admin/AdminUsersTable.tsx
Normal file
170
apps/website/components/admin/AdminUsersTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
93
apps/website/components/admin/BulkActionBar.tsx
Normal file
93
apps/website/components/admin/BulkActionBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
68
apps/website/components/admin/UserStatusTag.tsx
Normal file
68
apps/website/components/admin/UserStatusTag.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user