summaryrefslogtreecommitdiff
path: root/packages/tweetdeck/src/pages/Deck.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/tweetdeck/src/pages/Deck.tsx')
-rw-r--r--packages/tweetdeck/src/pages/Deck.tsx310
1 files changed, 310 insertions, 0 deletions
diff --git a/packages/tweetdeck/src/pages/Deck.tsx b/packages/tweetdeck/src/pages/Deck.tsx
new file mode 100644
index 0000000..c6fa41a
--- /dev/null
+++ b/packages/tweetdeck/src/pages/Deck.tsx
@@ -0,0 +1,310 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+import "../styles/normalize.css";
+import "../styles/index.css";
+import { Sidebar, type NewAccountInput } from "../components/Sidebar";
+import { ColumnBoard } from "../components/ColumnBoard";
+import { AddColumnModal } from "../components/AddColumnModal";
+import { usePersistentState } from "../hooks/usePersistentState";
+import type {
+ ColumnSnapshot,
+ ColumnState,
+ DeckAccount,
+ DeckColumn,
+ DeckListsCache,
+ FullscreenState,
+} from "../types/app";
+import type { Tweet } from "../lib/fetching/types";
+import { generateId } from "../lib/utils/id";
+import { twitterClient } from "../lib/client/twitterClient";
+import { FullscreenColumn } from "../components/FullscreenColumn";
+
+const ACCOUNTS_KEY = "tweetdeck.accounts";
+const COLUMNS_KEY = "tweetdeck.columns";
+
+export function App() {
+ const [accounts, setAccounts] = usePersistentState<DeckAccount[]>(
+ ACCOUNTS_KEY,
+ [],
+ );
+ const [columns, setColumns] = usePersistentState<DeckColumn[]>(
+ COLUMNS_KEY,
+ [],
+ );
+ const [listsCache, setListsCache] = useState<DeckListsCache>({});
+ const [activeAccountId, setActiveAccountId] = useState<string | undefined>(
+ () => accounts[0]?.id,
+ );
+ const [isModalOpen, setModalOpen] = useState(false);
+ const [toast, setToast] = useState<string | null>(null);
+ const [fullscreen, setFullscreen] = useState<FullscreenState | null>(null);
+ const [columnSnapshots, setColumnSnapshots] = useState<
+ Record<string, ColumnSnapshot>
+ >({});
+
+ useEffect(() => {
+ if (!activeAccountId) {
+ const firstAccount = accounts[0];
+ if (firstAccount) {
+ setActiveAccountId(firstAccount.id);
+ }
+ }
+ }, [accounts, activeAccountId]);
+
+ useEffect(() => {
+ const acs = accounts.filter((a) => !a.avatar || !a.username);
+ console.log({ acs });
+ const nacs = acs.map(async (acc) => {
+ const our = await twitterClient.own({ cookie: acc.cookie });
+ const nacc = {
+ ...acc,
+ handle: our.username,
+ label: our.name,
+ avatar: our.avatar,
+ };
+ return nacc;
+ });
+ Promise.all(nacs)
+ .then((acs) => setAccounts(acs))
+ .catch((err) => console.error(err));
+ }, []);
+
+ const handleAddAccount = useCallback(
+ (payload: NewAccountInput) => {
+ const label = `Session ${accounts.length + 1}`;
+ const account: DeckAccount = {
+ id: generateId(),
+ label,
+ accent: randomAccent(),
+ cookie: payload.cookie.trim(),
+ createdAt: Date.now(),
+ };
+ setAccounts((prev) => [...prev, account]);
+ setActiveAccountId(account.id);
+ setToast(`${account.label} is ready`);
+ },
+ [accounts.length, setAccounts],
+ );
+
+ const handleRemoveAccount = useCallback(
+ (accountId: string) => {
+ setAccounts((prev) => prev.filter((account) => account.id !== accountId));
+ setColumns((prev) =>
+ prev.filter((column) => column.accountId !== accountId),
+ );
+ setListsCache((prev) => {
+ const next = { ...prev };
+ delete next[accountId];
+ return next;
+ });
+ if (activeAccountId === accountId) {
+ setActiveAccountId(undefined);
+ }
+ },
+ [activeAccountId, setAccounts, setColumns],
+ );
+
+ const handleAddColumn = useCallback(
+ (column: Omit<DeckColumn, "id">) => {
+ const nextColumn = { ...column, id: generateId() };
+ setColumns((prev) => [...prev, nextColumn]);
+ setToast(`${nextColumn.title} added to deck`);
+ },
+ [setColumns],
+ );
+
+ const handleRemoveColumn = useCallback(
+ (id: string) => {
+ setColumns((prev) => prev.filter((column) => column.id !== id));
+ },
+ [setColumns],
+ );
+
+ const fetchLists = useCallback(
+ async (accountId: string) => {
+ const account = accounts.find((acc) => acc.id === accountId);
+ if (!account) throw new Error("Account not found");
+ if (listsCache[accountId]) return listsCache[accountId];
+ console.log({ listsCache });
+ const lists = await twitterClient.lists({ cookie: account.cookie });
+ console.log({ lists });
+ setListsCache((prev) => ({ ...prev, [accountId]: lists }));
+ return lists;
+ },
+ [accounts, listsCache],
+ );
+
+ const handleColumnStateChange = useCallback(
+ (columnId: string, state: ColumnState) => {
+ setColumns((prev) =>
+ prev.map((column) =>
+ column.id === columnId ? { ...column, state } : column,
+ ),
+ );
+ },
+ [setColumns],
+ );
+
+ const handleColumnSnapshot = useCallback(
+ (columnId: string, snapshot: ColumnSnapshot) => {
+ setColumnSnapshots((prev) => {
+ const existing = prev[columnId];
+ if (
+ existing &&
+ existing.tweets === snapshot.tweets &&
+ existing.label === snapshot.label
+ ) {
+ return prev;
+ }
+ return {
+ ...prev,
+ [columnId]: { tweets: snapshot.tweets, label: snapshot.label },
+ };
+ });
+ },
+ [],
+ );
+
+ const openFullscreen = useCallback(
+ (payload: FullscreenState) => {
+ const snapshot = columnSnapshots[payload.column.id];
+ setFullscreen({
+ ...payload,
+ tweets: snapshot?.tweets ?? payload.tweets,
+ columnLabel: snapshot?.label ?? payload.columnLabel,
+ });
+ },
+ [columnSnapshots],
+ );
+
+ useEffect(() => {
+ if (!fullscreen) return;
+ const snapshot = columnSnapshots[fullscreen.column.id];
+ if (!snapshot) return;
+ if (
+ snapshot.tweets === fullscreen.tweets &&
+ snapshot.label === fullscreen.columnLabel
+ ) {
+ return;
+ }
+ setFullscreen((prev) => {
+ if (!prev) return prev;
+ if (prev.column.id !== fullscreen.column.id) return prev;
+ const tweets = snapshot.tweets;
+ const index = Math.min(prev.index, Math.max(tweets.length - 1, 0));
+ return {
+ ...prev,
+ tweets,
+ columnTitle: snapshot.label,
+ index,
+ };
+ });
+ }, [columnSnapshots, fullscreen]);
+
+ const content = useMemo(
+ () => (
+ <ColumnBoard
+ columns={columns}
+ accounts={accounts}
+ onRemove={handleRemoveColumn}
+ onStateChange={handleColumnStateChange}
+ onSnapshot={handleColumnSnapshot}
+ onEnterFullscreen={openFullscreen}
+ />
+ ),
+ [
+ accounts,
+ columns,
+ handleRemoveColumn,
+ handleColumnStateChange,
+ handleColumnSnapshot,
+ openFullscreen,
+ ],
+ );
+
+ return (
+ <div className="app-shell">
+ <Sidebar
+ accounts={accounts}
+ activeAccountId={activeAccountId}
+ onActivate={(id) => setActiveAccountId(id)}
+ onAddAccount={handleAddAccount}
+ onRemoveAccount={handleRemoveAccount}
+ onAddColumn={() => setModalOpen(true)}
+ />
+
+ <main>{content}</main>
+
+ <AddColumnModal
+ accounts={accounts}
+ activeAccountId={activeAccountId}
+ isOpen={isModalOpen}
+ onClose={() => setModalOpen(false)}
+ onAdd={handleAddColumn}
+ fetchLists={fetchLists}
+ listsCache={listsCache}
+ />
+
+ {toast && (
+ <div className="toast" onAnimationEnd={() => setToast(null)}>
+ {toast}
+ </div>
+ )}
+
+ {fullscreen && (
+ <FullscreenColumn
+ state={fullscreen}
+ onExit={() => setFullscreen(null)}
+ onNavigate={(step) => {
+ setFullscreen((prev) => {
+ if (!prev) return prev;
+ if (!prev.tweets.length) return prev;
+ const nextIndex = Math.min(
+ prev.tweets.length - 1,
+ Math.max(0, prev.index + step),
+ );
+ if (nextIndex === prev.index) return prev;
+ return { ...prev, index: nextIndex };
+ });
+ }}
+ hasPrevColumn={fullscreen.columnIndex > 0}
+ hasNextColumn={fullscreen.columnIndex < columns.length - 1}
+ onSwitchColumn={(direction) => {
+ setFullscreen((prev) => {
+ if (!prev) return prev;
+ const nextIndex = prev.columnIndex + direction;
+ if (nextIndex < 0) return prev;
+ if (nextIndex >= columns.length) {
+ setModalOpen(true);
+ return prev;
+ }
+ const nextColumn = columns[nextIndex];
+ if (!nextColumn) return prev;
+ const snapshot = columnSnapshots[nextColumn.id];
+ const account = accounts.find(
+ (acc) => acc.id === nextColumn.accountId,
+ );
+ const tweets = snapshot?.tweets ?? [];
+ return {
+ column: nextColumn,
+ columnIndex: nextIndex,
+ columnLabel: snapshot?.label ?? nextColumn.title,
+ accent: account?.accent ?? prev.accent,
+ tweets,
+ index: 0,
+ };
+ });
+ }}
+ onAddColumn={() => setModalOpen(true)}
+ />
+ )}
+ </div>
+ );
+}
+
+function randomAccent(): string {
+ const palette = ["#7f5af0", "#2cb67d", "#f25f4c", "#f0a500", "#19a7ce"];
+ const pick = palette[Math.floor(Math.random() * palette.length)];
+ return pick ?? "#7f5af0";
+}
+
+export default App;