2026-06-14 01:29:44 +02:00
|
|
|
// Shared shell for the public, login-free legal pages (/privacy, /terms). These must be
|
|
|
|
|
// reachable unauthenticated — Google's OAuth consent screen links to them and reviewers
|
2026-07-01 12:46:50 +02:00
|
|
|
// fetch them directly — so they render outside the authenticated App tree (see main.tsx),
|
|
|
|
|
// with no react-query provider: the operator contact is fetched with a plain fetch below.
|
|
|
|
|
import { useEffect, useState } from "react";
|
2026-06-14 01:29:44 +02:00
|
|
|
|
2026-07-11 04:47:08 +02:00
|
|
|
const LAST_UPDATED = "June 14, 2026";
|
2026-06-14 01:29:44 +02:00
|
|
|
|
2026-07-01 12:46:50 +02:00
|
|
|
// Contact shown on the legal pages: the instance operator's configured admin email, from the
|
|
|
|
|
// public /auth/config. One shared fetch across the page; null when the operator set none.
|
|
|
|
|
let contactPromise: Promise<string | null> | null = null;
|
|
|
|
|
function fetchOperatorContact(): Promise<string | null> {
|
|
|
|
|
if (!contactPromise) {
|
|
|
|
|
contactPromise = fetch("/auth/config")
|
|
|
|
|
.then((r) => (r.ok ? r.json() : null))
|
|
|
|
|
.then((c) => (c?.operator_contact as string | undefined) ?? null)
|
|
|
|
|
.catch(() => null);
|
|
|
|
|
}
|
|
|
|
|
return contactPromise;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 04:47:08 +02:00
|
|
|
function useOperatorContact(): string | null {
|
2026-07-01 12:46:50 +02:00
|
|
|
const [contact, setContact] = useState<string | null>(null);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let alive = true;
|
|
|
|
|
fetchOperatorContact().then((c) => alive && setContact(c));
|
|
|
|
|
return () => {
|
|
|
|
|
alive = false;
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
return contact;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The operator's contact rendered as a mailto link, or a neutral fallback phrase when unset.
|
|
|
|
|
export function ContactEmail() {
|
|
|
|
|
const email = useOperatorContact();
|
|
|
|
|
if (!email) return <>your instance's administrator</>;
|
|
|
|
|
return (
|
|
|
|
|
<a href={`mailto:${email}`} className="text-accent hover:underline">
|
|
|
|
|
{email}
|
|
|
|
|
</a>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 01:29:44 +02:00
|
|
|
export function LegalLink({ href, children }: { href: string; children: React.ReactNode }) {
|
|
|
|
|
return (
|
|
|
|
|
<a href={href} target="_blank" rel="noreferrer" className="text-accent hover:underline">
|
|
|
|
|
{children}
|
|
|
|
|
</a>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function LegalLayout({
|
|
|
|
|
title,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
title: string;
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}) {
|
2026-07-01 12:46:50 +02:00
|
|
|
const contact = useOperatorContact();
|
2026-06-14 01:29:44 +02:00
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-bg text-fg">
|
|
|
|
|
<div className="mx-auto w-[min(92vw,720px)] py-12">
|
|
|
|
|
<a href="/" className="inline-block text-2xl font-bold tracking-tight mb-8">
|
chore: rebrand Subfeed -> Siftlode
Rename all user-facing references (UI wordmark Sift+lode, titles, app name,
legal pages, onboarding wizard, emails, README/docs) and infra paths
(/srv/subfeed -> /srv/siftlode, image tag, deploy script, backup filenames).
Internal identifiers kept on purpose: Postgres user/db "subfeed", logger
namespace, localStorage keys, and the subfeed_pgdata volume (renaming would
orphan the migrated production data).
2026-06-14 04:40:22 +02:00
|
|
|
Sift<span className="text-accent">lode</span>
|
2026-06-14 01:29:44 +02:00
|
|
|
</a>
|
|
|
|
|
<div className="glass rounded-2xl p-8">
|
|
|
|
|
<h1 className="text-2xl font-semibold">{title}</h1>
|
|
|
|
|
<p className="text-xs text-muted mt-1 mb-6">Last updated: {LAST_UPDATED}</p>
|
|
|
|
|
<div className="space-y-5 text-sm leading-relaxed text-fg/90">{children}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<footer className="mt-6 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted">
|
|
|
|
|
<a href="/" className="hover:text-fg transition">Home</a>
|
|
|
|
|
<a href="/privacy" className="hover:text-fg transition">Privacy Policy</a>
|
|
|
|
|
<a href="/terms" className="hover:text-fg transition">Terms of Service</a>
|
2026-07-01 12:46:50 +02:00
|
|
|
{contact && (
|
|
|
|
|
<a href={`mailto:${contact}`} className="hover:text-fg transition">Contact</a>
|
|
|
|
|
)}
|
2026-06-14 01:29:44 +02:00
|
|
|
</footer>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function H2({ children }: { children: React.ReactNode }) {
|
|
|
|
|
return <h2 className="text-base font-semibold text-fg pt-2">{children}</h2>;
|
|
|
|
|
}
|