Root causes:
- tia_admin_session is httpOnly so document.cookie could never read it → all
client-side cookie checks always failed and redirected before any data fetched
- Sub-pages used localStorage.getItem("admin_token") which was never stored,
and passed Authorization: Bearer null headers the server ignores
Fixes:
- FamilyProvider: use usePathname() hook instead of window.location.pathname
- admin/layout.tsx: rewrite as server component using verifyAdminSession()
(new lib/admin-auth.ts helper that uses next/headers cookies()) → server-side
redirect to /admin-login if session invalid; extract sidebar to AdminSidebar.tsx
- admin/page.tsx: remove broken document.cookie guard (layout handles auth now)
- admin-login/page.tsx: replace document.cookie check with GET /api/admin/auth call
- All 7 admin sub-pages: remove localStorage guard, remove Authorization: Bearer
headers, add credentials: include to every fetch call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
104 lines
No EOL
3.4 KiB
TypeScript
104 lines
No EOL
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
interface Child {
|
|
id: string;
|
|
name: string;
|
|
birthDate: string;
|
|
familyId: string;
|
|
familyName: string;
|
|
age: string;
|
|
}
|
|
|
|
export default function AdminChildren() {
|
|
const [children, setChildren] = useState<Child[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [search, setSearch] = useState("");
|
|
|
|
useEffect(() => {
|
|
fetchChildren();
|
|
}, []);
|
|
|
|
const fetchChildren = async () => {
|
|
try {
|
|
const res = await fetch("/api/admin/children", { credentials: "include" });
|
|
const data = await res.json();
|
|
setChildren(data.children || []);
|
|
} catch (err) {
|
|
console.error("Failed to fetch children:", err);
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
const filteredChildren = children.filter((c) =>
|
|
c.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
c.familyName.toLowerCase().includes(search.toLowerCase())
|
|
);
|
|
|
|
const exportCSV = () => {
|
|
const headers = ["Name", "Birth Date", "Family", "Age"];
|
|
const rows = filteredChildren.map((c) => [c.name, c.birthDate, c.familyName, c.age]);
|
|
const csv = [headers, ...rows].map((row) => row.join(",")).join("\n");
|
|
const blob = new Blob([csv], { type: "text/csv" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "children.csv";
|
|
a.click();
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="p-6 text-white">Loading...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Children</h1>
|
|
<p className="text-gray-400">{children.length} total children</p>
|
|
</div>
|
|
<button onClick={exportCSV} className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 rounded-lg">
|
|
Export CSV
|
|
</button>
|
|
</div>
|
|
|
|
<input
|
|
type="text"
|
|
placeholder="Search children..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-white"
|
|
/>
|
|
|
|
<div className="bg-gray-800 rounded-xl overflow-hidden">
|
|
<table className="w-full">
|
|
<thead className="bg-gray-700">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-sm font-medium">Child</th>
|
|
<th className="px-4 py-3 text-left text-sm font-medium">Birth Date</th>
|
|
<th className="px-4 py-3 text-left text-sm font-medium">Age</th>
|
|
<th className="px-4 py-3 text-left text-sm font-medium">Family</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-700">
|
|
{filteredChildren.map((child) => (
|
|
<tr key={child.id} className="hover:bg-gray-750">
|
|
<td className="px-4 py-3 font-medium">{child.name}</td>
|
|
<td className="px-4 py-3 text-sm text-gray-400">
|
|
{child.birthDate?.slice(0, 10)}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-gray-400">{child.age}</td>
|
|
<td className="px-4 py-3">{child.familyName}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{filteredChildren.length === 0 && (
|
|
<div className="p-8 text-center text-gray-500">No children found</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |