- Added `jose` library for JWT token generation and verification. - Implemented login functionality with token storage using cookies. - Created `HeaderAdmin` component for admin panel with login/logout capabilities. - Developed `LoginForm` component for user authentication. - Added `Table` component to display data with caching from localStorage. - Introduced `SubHeaderAdmin` for additional admin actions. - Enhanced `database.js` with functions for admin login and fetching table data. - Updated `server.js` to handle new routes for login and table data retrieval. - Modified `package.json` and `package-lock.json` to include new dependencies.
191 lines
6.3 KiB
TypeScript
191 lines
6.3 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import Cookies from "js-cookie";
|
|
import { getTableData, readCachedTableData } from "../utils/userHandler";
|
|
import { EllipsisVertical } from "lucide-react";
|
|
import SubHeaderAdmin from "./SubHeaderAdmin";
|
|
|
|
interface DataPackage {
|
|
losnummer: string;
|
|
vorname: string | null;
|
|
nachname: string | null;
|
|
adresse: string | null;
|
|
plz: string | null;
|
|
email: string | null;
|
|
[key: string]: any;
|
|
}
|
|
|
|
const Table: React.FC = () => {
|
|
const [rows, setRows] = useState<DataPackage[]>([]);
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Hilfsfunktion zum Einlesen & Normalisieren der LocalStorage-Daten
|
|
const loadFromCache = () => {
|
|
const cached = readCachedTableData<any>();
|
|
if (!cached) {
|
|
setRows([]);
|
|
return;
|
|
}
|
|
// Server könnte entweder ein Objekt oder ein Array liefern
|
|
const normalized: DataPackage[] = Array.isArray(cached)
|
|
? cached
|
|
: [cached];
|
|
setRows(normalized);
|
|
};
|
|
|
|
useEffect(() => {
|
|
// Initial lokale Daten laden (falls schon vorhanden)
|
|
loadFromCache();
|
|
|
|
// Frische Daten vom Backend holen
|
|
const token = Cookies.get("token") || "";
|
|
if (!token) return; // Kein Token => nur Cache anzeigen
|
|
|
|
setLoading(true);
|
|
getTableData(token)
|
|
.then((data) => {
|
|
if (data === null) {
|
|
setError("Fehler beim Laden der Daten.");
|
|
} else {
|
|
setError(null);
|
|
}
|
|
loadFromCache();
|
|
})
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
// Reagieren auf LocalStorage-Änderungen (z.B. in anderen Tabs)
|
|
useEffect(() => {
|
|
const handler = (e: StorageEvent) => {
|
|
if (e.key === "tableData") {
|
|
loadFromCache();
|
|
}
|
|
};
|
|
window.addEventListener("storage", handler);
|
|
return () => window.removeEventListener("storage", handler);
|
|
}, []);
|
|
|
|
const formatValue = (v: any) =>
|
|
v === null || v === undefined || v === "" ? "-" : String(v);
|
|
|
|
return (
|
|
<>
|
|
<SubHeaderAdmin />
|
|
<div className="w-full">
|
|
<div className="mb-4 flex items-center gap-3">
|
|
{loading && (
|
|
<span className="text-xs text-blue-600 animate-pulse">
|
|
Laden...
|
|
</span>
|
|
)}
|
|
{error && <span className="text-xs text-red-600">{error}</span>}
|
|
</div>
|
|
<div className="overflow-x-auto rounded-lg shadow ring-1 ring-black/5">
|
|
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th
|
|
scope="col"
|
|
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600"
|
|
>
|
|
<input type="checkbox" name="" id="" />
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600"
|
|
>
|
|
Losnummer
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600"
|
|
>
|
|
Vorname
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600"
|
|
>
|
|
Nachname
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600"
|
|
>
|
|
Adresse
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600"
|
|
>
|
|
PLZ
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600"
|
|
>
|
|
Email
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600"
|
|
></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100 bg-white">
|
|
{rows.length === 0 && !loading && (
|
|
<tr>
|
|
<td
|
|
colSpan={6}
|
|
className="px-4 py-6 text-center text-gray-500"
|
|
>
|
|
Keine Daten vorhanden.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
{rows.map((row, idx) => (
|
|
<tr
|
|
key={row.losnummer ?? idx}
|
|
className="hover:bg-gray-50 transition-colors"
|
|
>
|
|
<td className="px-4 py-2 font-mono text-xs text-gray-900">
|
|
<input type="checkbox" name="" id={row.losnummer} />
|
|
</td>
|
|
<td className="px-4 py-2 font-mono text-xs text-gray-900">
|
|
{formatValue(row.losnummer)}
|
|
</td>
|
|
<td className="px-4 py-2">
|
|
<input type="text" value={formatValue(row.vorname)} />
|
|
</td>
|
|
<td className="px-4 py-2">
|
|
<input type="text" value={formatValue(row.nachname)} />
|
|
</td>
|
|
<td
|
|
className="px-4 py-2 max-w-[16rem] truncate"
|
|
title={formatValue(row.adresse)}
|
|
>
|
|
<input type="text" value={formatValue(row.adresse)} />
|
|
</td>
|
|
<td className="px-4 py-2">
|
|
<input type="text" value={formatValue(row.plz)} />
|
|
</td>
|
|
<td className="px-4 py-2">
|
|
<input type="text" value={formatValue(row.email)} />
|
|
</td>
|
|
<td className="px-4 py-2">
|
|
<button>
|
|
<EllipsisVertical />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Table;
|