feat: add authentication and admin features
- 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.
This commit is contained in:
190
frontend/src/components/Table.tsx
Normal file
190
frontend/src/components/Table.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
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;
|
Reference in New Issue
Block a user