155 lines
5.3 KiB
TypeScript
155 lines
5.3 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import { Card } from '@/ui/Card';
|
|
import { Text } from '@/ui/Text';
|
|
import { Button } from '@/ui/Button';
|
|
import { Select } from '@/ui/Select';
|
|
import { Box } from '@/ui/Box';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Heading } from '@/ui/Heading';
|
|
import { Surface } from '@/ui/Surface';
|
|
import type { MembershipRole } from '@/lib/types/MembershipRole';
|
|
import type { LeagueRosterAdminViewData } from '@/lib/view-data/LeagueRosterAdminViewData';
|
|
|
|
interface RosterAdminTemplateProps {
|
|
viewData: LeagueRosterAdminViewData;
|
|
loading: boolean;
|
|
pendingCountLabel: string;
|
|
onApprove: (requestId: string) => Promise<void>;
|
|
onReject: (requestId: string) => Promise<void>;
|
|
onRoleChange: (driverId: string, newRole: MembershipRole) => Promise<void>;
|
|
onRemove: (driverId: string) => Promise<void>;
|
|
roleOptions: MembershipRole[];
|
|
}
|
|
|
|
export function RosterAdminTemplate({
|
|
viewData,
|
|
loading,
|
|
pendingCountLabel,
|
|
onApprove,
|
|
onReject,
|
|
onRoleChange,
|
|
onRemove,
|
|
roleOptions,
|
|
}: RosterAdminTemplateProps) {
|
|
const { joinRequests, members } = viewData;
|
|
|
|
return (
|
|
<Stack gap={6}>
|
|
<Card>
|
|
<Stack gap={6}>
|
|
<Box>
|
|
<Heading level={1}>Roster Admin</Heading>
|
|
<Text size="sm" color="text-gray-400" block mt={1}>
|
|
Manage join requests and member roles.
|
|
</Text>
|
|
</Box>
|
|
|
|
<Box>
|
|
<Stack direction="row" align="center" justify="between" mb={4}>
|
|
<Heading level={2}>Pending join requests</Heading>
|
|
<Text size="xs" color="text-gray-500">
|
|
{pendingCountLabel}
|
|
</Text>
|
|
</Stack>
|
|
|
|
{loading ? (
|
|
<Text size="sm" color="text-gray-400">Loading…</Text>
|
|
) : joinRequests.length > 0 ? (
|
|
<Stack gap={3}>
|
|
{joinRequests.map((req) => (
|
|
<Surface
|
|
key={req.id}
|
|
variant="muted"
|
|
rounded="lg"
|
|
border
|
|
padding={3}
|
|
>
|
|
<Stack direction="row" align="center" justify="between">
|
|
<Box>
|
|
<Text weight="medium" color="text-white" block>{req.driver.name}</Text>
|
|
<Text size="xs" color="text-gray-400" block mt={1}>{req.requestedAt}</Text>
|
|
{req.message && (
|
|
<Text size="xs" color="text-gray-500" block mt={1} truncate>{req.message}</Text>
|
|
)}
|
|
</Box>
|
|
|
|
<Stack direction="row" gap={2}>
|
|
<Button
|
|
onClick={() => onApprove(req.id)}
|
|
variant="primary"
|
|
size="sm"
|
|
>
|
|
Approve
|
|
</Button>
|
|
<Button
|
|
onClick={() => onReject(req.id)}
|
|
variant="secondary"
|
|
size="sm"
|
|
>
|
|
Reject
|
|
</Button>
|
|
</Stack>
|
|
</Stack>
|
|
</Surface>
|
|
))}
|
|
</Stack>
|
|
) : (
|
|
<Text size="sm" color="text-gray-500">No pending join requests.</Text>
|
|
)}
|
|
</Box>
|
|
|
|
<Box pt={6} borderTop borderColor="border-neutral-800">
|
|
<Box mb={4}>
|
|
<Heading level={2}>Members</Heading>
|
|
</Box>
|
|
|
|
{loading ? (
|
|
<Text size="sm" color="text-gray-400">Loading…</Text>
|
|
) : members.length > 0 ? (
|
|
<Stack gap={3}>
|
|
{members.map((member) => (
|
|
<Surface
|
|
key={member.driverId}
|
|
variant="muted"
|
|
rounded="lg"
|
|
border
|
|
padding={3}
|
|
>
|
|
<Stack direction="row" align="center" justify="between" wrap gap={4}>
|
|
<Box>
|
|
<Text weight="medium" color="text-white" block>{member.driver.name}</Text>
|
|
<Text size="xs" color="text-gray-400" block mt={1}>{member.joinedAt}</Text>
|
|
</Box>
|
|
|
|
<Stack direction="row" align="center" gap={3}>
|
|
<Box>
|
|
<Select
|
|
value={member.role}
|
|
onChange={(e) => onRoleChange(member.driverId, e.target.value as MembershipRole)}
|
|
options={roleOptions.map((role) => ({ value: role, label: role }))}
|
|
/>
|
|
</Box>
|
|
<Button
|
|
onClick={() => onRemove(member.driverId)}
|
|
variant="secondary"
|
|
size="sm"
|
|
>
|
|
Remove
|
|
</Button>
|
|
</Stack>
|
|
</Stack>
|
|
</Surface>
|
|
))}
|
|
</Stack>
|
|
) : (
|
|
<Text size="sm" color="text-gray-500">No members found.</Text>
|
|
)}
|
|
</Box>
|
|
</Stack>
|
|
</Card>
|
|
</Stack>
|
|
);
|
|
}
|