Files
gridpilot.gg/apps/website/components/teams/TeamAdmin.tsx
2025-12-11 21:06:25 +01:00

275 lines
9.1 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
import {
loadTeamAdminViewModel,
approveTeamJoinRequestAndReload,
rejectTeamJoinRequestAndReload,
updateTeamDetails,
type TeamAdminJoinRequestViewModel,
} from '@/lib/presenters/TeamAdminPresenter';
interface TeamAdminProps {
team: {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
};
onUpdate: () => void;
}
export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const [joinRequests, setJoinRequests] = useState<TeamAdminJoinRequestViewModel[]>([]);
const [requestDrivers, setRequestDrivers] = useState<Record<string, DriverDTO>>({});
const [loading, setLoading] = useState(true);
const [editMode, setEditMode] = useState(false);
const [editedTeam, setEditedTeam] = useState({
name: team.name,
tag: team.tag,
description: team.description,
});
useEffect(() => {
const load = async () => {
setLoading(true);
try {
const viewModel = await loadTeamAdminViewModel({
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: team.ownerId,
});
setJoinRequests(viewModel.requests);
const driversById: Record<string, DriverDTO> = {};
for (const request of viewModel.requests) {
if (request.driver) {
driversById[request.driverId] = request.driver;
}
}
setRequestDrivers(driversById);
} finally {
setLoading(false);
}
};
void load();
}, [team.id, team.name, team.tag, team.description, team.ownerId]);
const handleApprove = async (requestId: string) => {
try {
const updated = await approveTeamJoinRequestAndReload(requestId, team.id);
setJoinRequests(updated);
const driversById: Record<string, DriverDTO> = {};
for (const request of updated) {
if (request.driver) {
driversById[request.driverId] = request.driver;
}
}
setRequestDrivers(driversById);
onUpdate();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to approve request');
}
};
const handleReject = async (requestId: string) => {
try {
const updated = await rejectTeamJoinRequestAndReload(requestId, team.id);
setJoinRequests(updated);
const driversById: Record<string, DriverDTO> = {};
for (const request of updated) {
if (request.driver) {
driversById[request.driverId] = request.driver;
}
}
setRequestDrivers(driversById);
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to reject request');
}
};
const handleSaveChanges = async () => {
try {
await updateTeamDetails({
teamId: team.id,
name: editedTeam.name,
tag: editedTeam.tag,
description: editedTeam.description,
updatedByDriverId: team.ownerId,
});
setEditMode(false);
onUpdate();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to update team');
}
};
return (
<div className="space-y-6">
<Card>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-white">Team Settings</h3>
{!editMode && (
<Button variant="secondary" onClick={() => setEditMode(true)}>
Edit Details
</Button>
)}
</div>
{editMode ? (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Team Name
</label>
<Input
type="text"
value={editedTeam.name}
onChange={(e) => setEditedTeam({ ...editedTeam, name: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Team Tag
</label>
<Input
type="text"
value={editedTeam.tag}
onChange={(e) => setEditedTeam({ ...editedTeam, tag: e.target.value })}
maxLength={4}
/>
<p className="text-xs text-gray-500 mt-1">Max 4 characters</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Description
</label>
<textarea
className="w-full px-3 py-3 bg-iron-gray border-0 rounded-md text-white ring-1 ring-inset ring-charcoal-outline focus:ring-2 focus:ring-primary-blue transition-all duration-150 text-sm resize-none"
rows={4}
value={editedTeam.description}
onChange={(e) => setEditedTeam({ ...editedTeam, description: e.target.value })}
/>
</div>
<div className="flex gap-2">
<Button variant="primary" onClick={handleSaveChanges}>
Save Changes
</Button>
<Button
variant="secondary"
onClick={() => {
setEditMode(false);
setEditedTeam({
name: team.name,
tag: team.tag,
description: team.description,
});
}}
>
Cancel
</Button>
</div>
</div>
) : (
<div className="space-y-4">
<div>
<div className="text-sm text-gray-400">Team Name</div>
<div className="text-white font-medium">{team.name}</div>
</div>
<div>
<div className="text-sm text-gray-400">Team Tag</div>
<div className="text-white font-medium">{team.tag}</div>
</div>
<div>
<div className="text-sm text-gray-400">Description</div>
<div className="text-white">{team.description}</div>
</div>
</div>
)}
</Card>
<Card>
<h3 className="text-xl font-semibold text-white mb-6">Join Requests</h3>
{loading ? (
<div className="text-center py-8 text-gray-400">Loading requests...</div>
) : joinRequests.length > 0 ? (
<div className="space-y-3">
{joinRequests.map((request) => {
const driver = requestDrivers[request.driverId] ?? request.driver;
if (!driver) return null;
return (
<div
key={request.id}
className="flex items-center justify-between p-4 rounded-lg bg-deep-graphite border border-charcoal-outline"
>
<div className="flex items-center gap-4 flex-1">
<div className="w-12 h-12 rounded-full bg-primary-blue/20 flex items-center justify-center text-lg font-bold text-white">
{driver.name.charAt(0)}
</div>
<div className="flex-1">
<h4 className="text-white font-medium">{driver.name}</h4>
<p className="text-sm text-gray-400">
{driver.country} Requested {new Date(request.requestedAt).toLocaleDateString()}
</p>
{request.message && (
<p className="text-sm text-gray-300 mt-1 italic">
"{request.message}"
</p>
)}
</div>
</div>
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => handleApprove(request.id)}
>
Approve
</Button>
<Button
variant="danger"
onClick={() => handleReject(request.id)}
>
Reject
</Button>
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-8 text-gray-400">
No pending join requests
</div>
)}
</Card>
<Card>
<h3 className="text-xl font-semibold text-white mb-4">Danger Zone</h3>
<div className="space-y-4">
<div className="p-4 rounded-lg bg-danger-red/10 border border-danger-red/30">
<h4 className="text-white font-medium mb-2">Disband Team</h4>
<p className="text-sm text-gray-400 mb-4">
Permanently delete this team. This action cannot be undone.
</p>
<Button variant="danger" disabled>
Disband Team (Coming Soon)
</Button>
</div>
</div>
</Card>
</div>
);
}