diff options
Diffstat (limited to 'packages/tweetdeck/src/App.tsx')
| -rw-r--r-- | packages/tweetdeck/src/App.tsx | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/packages/tweetdeck/src/App.tsx b/packages/tweetdeck/src/App.tsx new file mode 100644 index 0000000..924ff9a --- /dev/null +++ b/packages/tweetdeck/src/App.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; |
