xtap-pool / explorer /src /App.tsx
osolmaz's picture
osolmaz HF Staff
deploy: eaa4b86
879c715 verified
Raw
History Blame Contribute Delete
8.7 kB
import { useEffect, useState } from "react";
import { AdminPanel } from "./components/AdminPanel.js";
import { FreeLabelPage } from "./components/FreeLabelPage.js";
import { FiltersPanel } from "./components/Filters.js";
import { Feed } from "./components/Feed.js";
import { GraphPage } from "./components/GraphPage.js";
import type { FreeLabelSummary, ContributorStats, Filters, LabelStat } from "./lib/api.js";
import {
defaultFilters,
fetchFreeLabels,
fetchContributors,
fetchLabels,
fetchMe,
} from "./lib/api.js";
import type { Route } from "./lib/router.js";
import { navigate, useRoute } from "./lib/router.js";
type AuthState =
| { status: "checking" }
| { status: "signed-out" }
| { status: "signed-in"; username: string; isAdmin: boolean };
type View = "feed" | "install" | "admin";
const INSTALL_COMMAND =
'rm -rf ~/.local/share/xtap-pool-extension && mkdir -p ~/.local/share/xtap-pool-extension && curl -L https://github.com/osolmaz/xtap-pool/archive/refs/heads/main.tar.gz | tar -xz --strip-components=2 -C ~/.local/share/xtap-pool-extension xtap-pool-main/extension && open -a "Google Chrome" "chrome://extensions"';
function InstallExtension(): React.JSX.Element {
const [copied, setCopied] = useState(false);
const copyCommand = (): void => {
void navigator.clipboard.writeText(INSTALL_COMMAND).then(
() => {
setCopied(true);
window.setTimeout(() => {
setCopied(false);
}, 1500);
},
() => undefined,
);
};
return (
<section className="flex flex-col gap-4 p-4">
<header className="border-b border-(--x-border) pb-4">
<h2 className="text-lg font-bold">Install extension</h2>
<p className="mt-1 text-sm text-(--x-muted)">
Add the browser extension, connect it to this pool, then browse X normally.
</p>
</header>
<p className="text-sm text-(--x-muted)">
Run this on macOS, enable Developer mode in Chrome, then Load unpacked and choose{" "}
<code>~/.local/share/xtap-pool-extension</code>.
</p>
<pre className="overflow-x-auto rounded-md border border-(--x-border) bg-(--x-soft) p-3 text-xs leading-5">
<code>{INSTALL_COMMAND}</code>
</pre>
<div className="flex flex-wrap gap-2 text-sm">
<button
className="rounded-md border border-(--x-border) px-3 py-1.5 font-semibold"
type="button"
onClick={copyCommand}
>
{copied ? "Copied" : "Copy command"}
</button>
<a
className="rounded-md bg-(--x-accent) px-3 py-1.5 font-semibold text-white"
href="/connect"
>
Connect
</a>
</div>
</section>
);
}
function SignIn(): React.JSX.Element {
return (
<main className="mx-auto flex max-w-md flex-col items-center gap-4 px-4 py-24 text-center">
<h1 className="text-2xl font-bold">xtap-pool</h1>
<p className="text-(--x-muted)">
A private tweet pool for friends. Sign in with your Hugging Face account to explore.
</p>
<a
className="rounded-full bg-(--x-accent) px-5 py-2 font-semibold text-white"
href="/oauth/login?next=/"
target="_blank"
rel="noreferrer"
>
Sign in with Hugging Face
</a>
</main>
);
}
type SidebarProps = {
username: string;
isAdmin: boolean;
view: View;
onGraphRoute: boolean;
filters: Filters;
contributors: readonly ContributorStats[];
labels: readonly LabelStat[];
freeLabels: readonly FreeLabelSummary[];
onView: (view: View) => void;
onFilters: (filters: Filters) => void;
};
/** Left rail: identity, view tabs (including the graph route) and filters. */
function Sidebar({
username,
isAdmin,
view,
onGraphRoute,
filters,
contributors,
labels,
freeLabels,
onView,
onFilters,
}: SidebarProps): React.JSX.Element {
const tabClass = (active: boolean, tone: "default" | "accent" = "default"): string =>
[
"rounded-md border px-3 py-1.5 text-sm font-semibold",
tone === "accent" ? "border-(--x-accent) text-(--x-accent)" : "border-(--x-border)",
active ? (tone === "accent" ? "bg-(--x-accent) text-white" : "bg-(--x-soft-active)") : "",
]
.filter(Boolean)
.join(" ");
const feedActive = !onGraphRoute && view === "feed";
const installActive = !onGraphRoute && view === "install";
const adminActive = !onGraphRoute && view === "admin";
return (
<aside>
<header className="mb-4">
<h1 className="text-xl font-bold">xtap-pool</h1>
<p className="text-sm text-(--x-muted)">signed in as @{username}</p>
</header>
<nav className="mb-4 flex flex-wrap gap-2">
<button
type="button"
aria-pressed={feedActive}
className={tabClass(feedActive)}
onClick={() => {
onView("feed");
}}
>
Feed
</button>
<button
type="button"
aria-pressed={onGraphRoute}
className={tabClass(onGraphRoute)}
onClick={() => {
navigate("/graph");
}}
>
Graph
</button>
<button
type="button"
aria-pressed={installActive}
className={tabClass(installActive, "accent")}
onClick={() => {
onView("install");
}}
>
Install
</button>
{isAdmin ? (
<button
type="button"
aria-pressed={adminActive}
className={tabClass(adminActive)}
onClick={() => {
onView("admin");
}}
>
Admin
</button>
) : null}
</nav>
{feedActive ? (
<FiltersPanel
filters={filters}
contributors={contributors}
labels={labels}
freeLabels={freeLabels}
onChange={onFilters}
/>
) : null}
</aside>
);
}
type MainContentProps = {
route: Route;
view: View;
isAdmin: boolean;
filters: Filters;
};
/** Active main pane: graph routes win over the feed-shell view tabs. */
function MainContent({ route, view, isAdmin, filters }: MainContentProps): React.JSX.Element {
if (route.kind === "free-label") return <FreeLabelPage key={route.slug} slug={route.slug} />;
if (route.kind === "graph") return <GraphPage />;
if (view === "install") return <InstallExtension />;
if (view === "admin" && isAdmin) return <AdminPanel />;
return <Feed filters={filters} />;
}
/** Root explorer app: auth gate, routing, filter rail and tweet feed. */
export function App(): React.JSX.Element {
const [auth, setAuth] = useState<AuthState>({ status: "checking" });
const [view, setView] = useState<View>("feed");
const [filters, setFilters] = useState<Filters>(defaultFilters);
const [contributors, setContributors] = useState<readonly ContributorStats[]>([]);
const [labels, setLabels] = useState<readonly LabelStat[]>([]);
const [freeLabels, setFreeLabels] = useState<readonly FreeLabelSummary[]>([]);
const route = useRoute();
useEffect(() => {
void (async (): Promise<void> => {
try {
const me = await fetchMe();
setAuth(
me === undefined
? { status: "signed-out" }
: { status: "signed-in", username: me.username, isAdmin: me.isAdmin },
);
} catch {
setAuth({ status: "signed-out" });
}
})();
}, []);
useEffect(() => {
if (auth.status !== "signed-in") return;
void fetchContributors().then(setContributors, () => undefined);
void fetchLabels().then(setLabels, () => undefined);
void fetchFreeLabels().then(setFreeLabels, () => undefined);
}, [auth.status]);
if (auth.status === "checking") {
return <p className="p-8 text-sm text-(--x-muted)">Loading…</p>;
}
if (auth.status === "signed-out") {
return <SignIn />;
}
const selectView = (next: View): void => {
setView(next);
if (route.kind !== "home") navigate("/");
};
return (
<div className="mx-auto grid max-w-4xl grid-cols-1 gap-6 px-4 py-6 md:grid-cols-[14rem_minmax(0,1fr)]">
<Sidebar
username={auth.username}
isAdmin={auth.isAdmin}
view={view}
onGraphRoute={route.kind !== "home"}
filters={filters}
contributors={contributors}
labels={labels}
freeLabels={freeLabels}
onView={selectView}
onFilters={setFilters}
/>
<main className="border-x border-(--x-border)">
<MainContent route={route} view={view} isAdmin={auth.isAdmin} filters={filters} />
</main>
</div>
);
}