game_list/frontend/src/PersonList.tsx
2026-01-11 20:32:34 +01:00

209 lines
6.4 KiB
TypeScript

import { Person } from "../items";
import { Link } from "react-router-dom";
import { useState, useEffect, useMemo } from "react";
import { get_auth_status, refresh_state, get_is_admin } from "./api";
import type { ToastType } from "./Toast";
import { EmptyState } from "./components/EmptyState";
import "./PersonList.css"
interface Props {
people: Person[];
loading?: boolean;
onShowToast?: (message: string, type?: ToastType) => void;
}
export const PersonList = ({ people, loading = false, onShowToast }: Props) => {
const [current_user, set_current_user] = useState<string>("");
const [isRefreshing, setIsRefreshing] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => {
get_auth_status().then((res) => {
if (res) {
set_current_user(res.username);
}
});
}, []);
const filteredPeople = useMemo(() => {
if (!searchQuery) return people;
return people.filter(person =>
person.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [people, searchQuery]);
const handleRefresh = async () => {
setIsRefreshing(true);
try {
await refresh_state();
onShowToast?.("State refreshed from file successfully", "success");
window.location.reload();
} catch (err) {
console.error(err);
onShowToast?.("Failed to refresh state from file", "error");
} finally {
setIsRefreshing(false);
}
};
const isAdmin = get_is_admin();
return (
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "1rem",
flexWrap: "wrap",
gap: "1rem"
}}
>
<h2>People List {filteredPeople.length > 0 && <span style={{ fontSize: "0.8em", color: "var(--text-muted)" }}>({filteredPeople.length})</span>}</h2>
<div style={{ display: "flex", gap: "0.75rem", alignItems: "center" }}>
<input
type="text"
placeholder="🔍 Search people..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{
padding: "0.5rem 1rem",
fontSize: "0.9rem",
minWidth: "200px"
}}
/>
{isAdmin && (
<button
onClick={handleRefresh}
disabled={isRefreshing}
className="btn-secondary"
style={{
padding: "0.5rem 1rem",
fontSize: "0.9rem",
display: "flex",
alignItems: "center",
gap: "0.5rem",
opacity: isRefreshing ? 0.7 : 1,
cursor: isRefreshing ? "not-allowed" : "pointer",
}}
>
{isRefreshing ? (
<>
<span
style={{
width: "14px",
height: "14px",
border: "2px solid rgba(255,255,255,0.3)",
borderTopColor: "currentColor",
borderRadius: "50%",
animation: "spin 0.8s linear infinite",
}}
></span>
Refreshing...
</>
) : (
<>
<span>🔄</span>
Refresh from File
</>
)}
</button>
)}
</div>
</div>
{loading ? (
<div className="grid-container">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
style={{
backgroundColor: "var(--secondary-alt-bg)",
border: "1px solid var(--border-color)",
borderRadius: "5px",
padding: "10px",
minHeight: "60px",
animation: "shimmer 1.5s infinite"
}}
>
<div
style={{
width: "60%",
height: "20px",
backgroundColor: "var(--tertiary-bg)",
borderRadius: "4px",
marginBottom: "0.5rem",
animation: "shimmer 1.5s infinite"
}}
></div>
<div
style={{
width: "40%",
height: "16px",
backgroundColor: "var(--tertiary-bg)",
borderRadius: "4px",
animation: "shimmer 1.5s infinite 0.2s"
}}
></div>
</div>
))}
<style>{`
@keyframes shimmer {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
`}</style>
</div>
) : filteredPeople.length === 0 ? (
<EmptyState
icon="👥"
title={searchQuery ? "No people found" : "No people in list"}
description={searchQuery ? "Try a different search term" : "Add people to get started"}
/>
) : (
<div className="grid-container">
{filteredPeople.map((person, index) => {
if (person.name.toLowerCase() === current_user.toLowerCase()) {
return (
<Link
to={`/games#existing-games`}
key={index}
className="list-item"
style={{
textDecoration: "none",
color: "inherit",
display: "block",
}}
>
<h3>{person.name}</h3>
<div style={{ fontSize: "0.85em", color: "var(--text-muted)", marginTop: "0.25rem" }}>
{person.opinion.length} opinion(s)
</div>
</Link>
);
}
return (
<Link
to={`/person/${person.name}`}
key={index}
className="list-item"
style={{
textDecoration: "none",
color: "inherit",
display: "block",
}}
>
<h3>{person.name}</h3>
<div style={{ fontSize: "0.85em", color: "var(--text-muted)", marginTop: "0.25rem" }}>
{person.opinion.length} opinion(s)
</div>
</Link>
);
})}
</div>
)}
</div>
);
};