Files
music-repo/packages/frontend/src/routes/Admin.tsx
2025-10-11 23:14:33 +02:00

184 lines
5.2 KiB
TypeScript

import { userCache } from "@/cache";
import { client } from "@/client";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useBreakpoint } from "@/hooks/useBreakpoint";
import { useCache } from "@/hooks/useCache";
import { useLoading } from "@/hooks/useLoading";
import { DEBOUNCE } from "@/snippets";
import { UserId } from "common";
import { Role } from "common/the_api";
import { Array, Effect, HashSet, Match, Option, pipe } from "effect";
import { Loader2 } from "lucide-react";
import { useRef, useState } from "react";
export function Admin() {
const [displayName, setDisplayName] = useState("");
const [role, setRole] = useState<Role | "">("");
const debounce = useRef(Effect.void);
const breakpoint = useBreakpoint();
const columns = breakpoint ? 4 : 1;
const { isLoading, error, data: userIds } = useLoading(Effect.gen(function* () {
yield* debounce.current;
const data = yield* client.queryUsers({
displayName: displayName !== "" ? Option.some(displayName) : Option.none(),
role: role !== "" ? Option.some(role) : Option.none(),
offset: 0,
limit: 100,
});
return data;
}), [displayName, role]);
return (
<div className="p-4 overflow-y-auto flex flex-col gap-4">
<div className="flex flex-row flex-wrap items-baseline gap-4">
<Input
className="w-full md:w-[32ch]"
type="text"
placeholder="Nazwa wyświetlana"
value={displayName}
onChange={(e) => {
setDisplayName(e.target.value);
debounce.current = DEBOUNCE;
}}
/>
<Select
value={role}
onValueChange={(role) => setRole(role === "None" ? "" : role as Role)}
>
<SelectTrigger className="w-full md:w-[24ch]">
<SelectValue placeholder="Rola" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Role</SelectLabel>
<SelectItem value="None"><em>Dowolna</em></SelectItem>
<SelectItem value={Role.Viewer}>Przeglądanie</SelectItem>
<SelectItem value={Role.Editor}>Edytor</SelectItem>
<SelectItem value={Role.Admin}>Administrator</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<Table>
{breakpoint && (
<TableHeader className="bg-white sticky top-0">
<TableRow>
<TableHead className="w-10" />
<TableHead>Nazwa wyświetlana</TableHead>
<TableHead>ID</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
)}
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns} >
<div className="flex items-center justify-center gap-2">
<Loader2 className="animate-spin" />
Ładowanie
</div>
</TableCell>
</TableRow>
) : error !== null ? (
<TableRow>
<TableCell colSpan={columns} >
<div className="flex items-center justify-center gap-2">
Wystąpił błąd: {Match.value(error).pipe(
Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
Match.exhaustive,
)}
</div>
</TableCell>
</TableRow>
) : (
userIds.map((userId) => <UserRow key={userId} userId={userId} />)
)}
</TableBody>
</Table>
</div>
);
}
namespace UserRow {
export interface Props {
readonly userId: UserId;
}
}
function UserRow(props: UserRow.Props) {
const { isLoading, error, data: user } = useCache(userCache, props.userId);
const breakpoint = useBreakpoint();
const columns = breakpoint ? 4 : 1;
if (isLoading) {
return (
<TableRow>
<TableCell colSpan={columns}>Ładowanie</TableCell>
</TableRow>
);
}
if (error !== null) {
return (
<TableRow>
<TableCell colSpan={columns}>
Wystąpił błąd: {Match.value(error).pipe(
Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
Match.tag("NotFound", () => "Użytkownik nie istnieje"),
Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
Match.exhaustive,
)}
</TableCell>
</TableRow>
);
}
const rolesText = pipe(
user.roles,
HashSet.values,
Array.join(", "),
);
return (
<TableRow>
{breakpoint ? (<>
<TableCell>
<Avatar className="shadow">
<AvatarImage src={Option.getOrUndefined(user.avatarUrl)} />
</Avatar>
</TableCell>
<TableCell>
{Option.getOrUndefined(user.displayName)}
</TableCell>
<TableCell className="font-mono">
{user.userId}
</TableCell>
<TableCell>
{rolesText}
</TableCell>
</>) : (
<TableCell className="flex flex-col">
<div className="flex flex-row items-center gap-2 mb-2">
<Avatar className="shadow">
<AvatarImage src={Option.getOrUndefined(user.avatarUrl)} />
</Avatar>
<div>{Option.getOrUndefined(user.displayName)}</div>
</div>
<div className="text-xs font-mono">{user.userId}</div>
<div className="text-xs">{rolesText}</div>
</TableCell>
)}
</TableRow>
);
}