feat: Add share-link backend and wire up the sheet
Add a small Go + SQLite service (server/) that stores character-sheet JSON blobs and returns short, content-addressed IDs, so sheets can be shared via a compact ?s=<id> link instead of an oversized inline payload. CharacterSheet.tsx now POSTs to the service on share and fetches by ID on load, falling back to the self-contained ?c= inline link when the backend is unreachable. Legacy ?c= links still decode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,13 @@ function pruneEmpty(data: Record<string, unknown>) {
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve a share-service path relative to the current page so it works under
|
||||
// any base path (e.g. /fabula/) where Caddy reverse-proxies /api/* to the
|
||||
// backend. Returns e.g. https://host/fabula/api/s.
|
||||
function apiURL(path: string) {
|
||||
return new URL(path, window.location.href).toString();
|
||||
}
|
||||
|
||||
export default function CharacterSheet() {
|
||||
const [activeTab, setActiveTab] = useState("main");
|
||||
const [urlMode, setUrlMode] = useState(false);
|
||||
@@ -397,8 +404,23 @@ export default function CharacterSheet() {
|
||||
|
||||
// Init
|
||||
useEffect(() => {
|
||||
const encoded = new URLSearchParams(window.location.search).get("c");
|
||||
if (encoded) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const shortId = params.get("s");
|
||||
const encoded = params.get("c");
|
||||
if (shortId) {
|
||||
// Short link: fetch the sheet JSON from the share service.
|
||||
fetch(apiURL("api/s/" + encodeURIComponent(shortId)))
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
applyData(data);
|
||||
setUrlMode(true);
|
||||
})
|
||||
.catch(() => tryAutoLoad());
|
||||
} else if (encoded) {
|
||||
// Legacy/offline self-contained link with the data inline.
|
||||
decompressFromBase64(encoded)
|
||||
.then((json) => {
|
||||
applyData(JSON.parse(json));
|
||||
@@ -477,12 +499,24 @@ export default function CharacterSheet() {
|
||||
);
|
||||
|
||||
const copyShareURL = useCallback(async () => {
|
||||
const encoded = await compressToBase64(
|
||||
JSON.stringify(pruneEmpty(collectData())),
|
||||
);
|
||||
await navigator.clipboard.writeText(
|
||||
window.location.origin + window.location.pathname + "?c=" + encoded,
|
||||
);
|
||||
const json = JSON.stringify(pruneEmpty(collectData()));
|
||||
const base = window.location.origin + window.location.pathname;
|
||||
let shareURL: string;
|
||||
try {
|
||||
// Preferred: store the sheet server-side and share a short ?s=<id> link.
|
||||
const res = await fetch(apiURL("api/s"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: json,
|
||||
});
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
const { id } = await res.json();
|
||||
shareURL = base + "?s=" + id;
|
||||
} catch {
|
||||
// Backend unreachable: fall back to a self-contained inline ?c= link.
|
||||
shareURL = base + "?c=" + (await compressToBase64(json));
|
||||
}
|
||||
await navigator.clipboard.writeText(shareURL);
|
||||
setCopyStatus(true);
|
||||
setTimeout(() => setCopyStatus(false), 2000);
|
||||
}, [collectData]);
|
||||
|
||||
Reference in New Issue
Block a user