209 lines
6.4 KiB
TypeScript
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>
|
|
);
|
|
};
|