mirror of
https://github.com/Chewbaccalakis/rfid-database.git
synced 2026-09-09 16:01:56 -07:00
Add Next.js app with SQLite/Drizzle storage for sites and LF/HF tag dumps, multi-user Auth.js (credentials + optional OIDC), personal API tokens in Settings, versioned /api/v1 for UI and future CLI use, MCT/Proxmark/JSON import-export, PWA offline shell, Vitest tests, and GitHub Actions CI.
103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useEffect, useState } from "react";
|
|
import { useParams, useRouter } from "next/navigation";
|
|
import { formatUid } from "@/lib/rfid/uid";
|
|
|
|
type Site = { id: string; name: string; code: string; notes: string | null };
|
|
type Tag = {
|
|
id: string;
|
|
label: string;
|
|
frequency: string;
|
|
protocol: string;
|
|
uid: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export default function SitePage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const router = useRouter();
|
|
const [site, setSite] = useState<Site | null>(null);
|
|
const [tags, setTags] = useState<Tag[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
async function load() {
|
|
const [siteRes, tagsRes] = await Promise.all([
|
|
fetch(`/api/v1/sites/${id}`),
|
|
fetch(`/api/v1/tags?siteId=${id}`),
|
|
]);
|
|
if (siteRes.status === 401 || tagsRes.status === 401) {
|
|
router.push("/login");
|
|
return;
|
|
}
|
|
if (!siteRes.ok) {
|
|
setError("Site not found");
|
|
return;
|
|
}
|
|
setSite(await siteRes.json());
|
|
const tagData = await tagsRes.json();
|
|
setTags(tagData.tags);
|
|
}
|
|
load();
|
|
}, [id, router]);
|
|
|
|
async function deleteSite() {
|
|
if (!confirm("Delete this site and all its tags?")) return;
|
|
const res = await fetch(`/api/v1/sites/${id}`, { method: "DELETE" });
|
|
if (!res.ok) {
|
|
setError("Failed to delete site");
|
|
return;
|
|
}
|
|
router.push("/");
|
|
}
|
|
|
|
if (error) return <div className="error-banner">{error}</div>;
|
|
if (!site) return <p className="muted">Loading…</p>;
|
|
|
|
return (
|
|
<div>
|
|
<div className="row" style={{ justifyContent: "space-between", marginBottom: "1rem" }}>
|
|
<div>
|
|
<div className="row" style={{ gap: "0.5rem", marginBottom: "0.35rem" }}>
|
|
<span className="badge badge-accent">{site.code}</span>
|
|
<h1 className="page-title" style={{ margin: 0 }}>
|
|
{site.name}
|
|
</h1>
|
|
</div>
|
|
{site.notes && <p className="page-sub">{site.notes}</p>}
|
|
</div>
|
|
<div className="row">
|
|
<Link className="btn" href={`/tags/new?siteId=${site.id}`}>
|
|
Add tag
|
|
</Link>
|
|
<button type="button" className="btn btn-danger" onClick={deleteSite}>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="panel" style={{ padding: 0, overflow: "hidden" }}>
|
|
{tags.length === 0 ? (
|
|
<p className="muted" style={{ padding: "1.25rem" }}>
|
|
No tags at this site yet.
|
|
</p>
|
|
) : (
|
|
tags.map((tag) => (
|
|
<Link key={tag.id} href={`/tags/${tag.id}`} className="list-row">
|
|
<div>
|
|
<strong>{tag.label}</strong>
|
|
<div className="muted mono" style={{ fontSize: "0.85rem", marginTop: "0.2rem" }}>
|
|
{formatUid(tag.uid)} · {tag.protocol.replaceAll("_", " ")}
|
|
</div>
|
|
</div>
|
|
<span className="badge">{tag.frequency}</span>
|
|
</Link>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|