mirror of
https://github.com/Chewbaccalakis/rfid-database.git
synced 2026-09-10 00:11:56 -07:00
Implement RFID tag dump PWA with REST API, auth, and CI
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.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user