diff options
Diffstat (limited to 'packages/tweetdeck/src/components/AddColumnModal.tsx')
| -rw-r--r-- | packages/tweetdeck/src/components/AddColumnModal.tsx | 234 |
1 files changed, 234 insertions, 0 deletions
diff --git a/packages/tweetdeck/src/components/AddColumnModal.tsx b/packages/tweetdeck/src/components/AddColumnModal.tsx new file mode 100644 index 0000000..b7098ed --- /dev/null +++ b/packages/tweetdeck/src/components/AddColumnModal.tsx @@ -0,0 +1,234 @@ +import { useEffect, useMemo, useState } from "react"; +import type { DeckAccount, DeckColumn, DeckListsCache } from "../types/app"; +import type { TwitterList } from "../lib/fetching/types"; + +interface AddColumnModalProps { + accounts: DeckAccount[]; + isOpen: boolean; + onClose: () => void; + onAdd: (column: Omit<DeckColumn, "id">) => void; + activeAccountId?: string; + fetchLists: (accountId: string) => Promise<TwitterList[]>; + listsCache: DeckListsCache; +} + +const columnOptions = [ + { + id: "foryou", + label: "For You", + description: "Twitter's AI-ranked firehose", + }, + { + id: "following", + label: "Following", + description: "Chronological feed of people you follow", + }, + { id: "bookmarks", label: "Bookmarks", description: "Your saved deep dives" }, + { id: "list", label: "List", description: "Curate a topic-specific stream" }, + { id: "chat", label: "Chat", description: "Mentions & notifications" }, +] as const; + +export function AddColumnModal({ + accounts, + activeAccountId, + isOpen, + onClose, + onAdd, + fetchLists, + listsCache, +}: AddColumnModalProps) { + const [kind, setKind] = useState<DeckColumn["kind"]>("foryou"); + const [accountId, setAccountId] = useState<string>( + activeAccountId || accounts[0]?.id || "", + ); + const [title, setTitle] = useState("For You"); + const [listId, setListId] = useState<string>(""); + const [listOptions, setListOptions] = useState<TwitterList[]>([]); + const [listsLoading, setListsLoading] = useState(false); + const [listsError, setListsError] = useState<string | undefined>(); + + useEffect(() => { + if (!isOpen) return; + setAccountId(activeAccountId || accounts[0]?.id || ""); + }, [isOpen, activeAccountId, accounts]); + + useEffect(() => { + setTitle(defaultTitle(kind)); + }, [kind]); + + useEffect(() => { + if (!isOpen || kind !== "list" || !accountId) return; + let mounted = true; + async function loadLists() { + try { + setListsLoading(true); + setListsError(undefined); + const cached = listsCache[accountId]; + if (cached) { + setListOptions(cached); + return; + } + const lists = await fetchLists(accountId); + if (!mounted) return; + setListOptions(lists); + } catch (error) { + setListsError( + error instanceof Error ? error.message : "Failed to load lists", + ); + } finally { + if (mounted) setListsLoading(false); + } + } + loadLists(); + return () => { + mounted = false; + }; + }, [accountId, fetchLists, isOpen, kind, listsCache]); + + useEffect(() => { + if (!isOpen) return; + setListId(""); + setListOptions([]); + }, [accountId, isOpen, kind]); + + const selectedAccount = useMemo( + () => accounts.find((account) => account.id === accountId), + [accountId, accounts], + ); + + const canSubmit = Boolean(selectedAccount && (kind !== "list" || listId)); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + if (!canSubmit || !selectedAccount) return; + onAdd({ + accountId: selectedAccount.id, + account: selectedAccount.username || "", + kind, + title: title.trim() || defaultTitle(kind), + listId: listId || undefined, + listName: + kind === "list" + ? listOptions.find((list) => list.id === listId)?.name + : undefined, + }); + onClose(); + }; + + useEffect(() => { + if (!isOpen) return; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") onClose(); + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( + <div + className="modal-backdrop" + role="dialog" + aria-modal="true" + onMouseDown={(event) => { + if (event.target === event.currentTarget) { + onClose(); + } + }} + > + <div className="modal"> + <header> + <div> + <p className="eyebrow">New column</p> + <h2>Design your stream</h2> + </div> + <button + className="ghost" + onClick={onClose} + aria-label="Close add column modal" + > + × + </button> + </header> + <form onSubmit={handleSubmit} className="modal-body"> + <label> + Account + <select + value={accountId} + onChange={(e) => setAccountId(e.target.value)} + > + {accounts.map((account) => ( + <option value={account.id} key={account.id}> + {account.label} + {account.handle ? ` (@${account.handle})` : ""} + </option> + ))} + </select> + </label> + <div className="option-grid"> + {columnOptions.map((option) => ( + <button + type="button" + key={option.id} + className={`option ${kind === option.id ? "selected" : ""}`} + onClick={() => setKind(option.id)} + > + <div> + <strong>{option.label}</strong> + <p>{option.description}</p> + </div> + </button> + ))} + </div> + {kind === "list" && ( + <label> + Choose a list + {listsError && <span className="error">{listsError}</span>} + <select + value={listId} + disabled={listsLoading} + onChange={(e) => setListId(e.target.value)} + > + <option value="" disabled> + {listsLoading ? "Loading lists..." : "Select a list"} + </option> + {listOptions.map((list) => ( + <option key={list.id} value={list.id}> + {list.name} ({list.member_count}) + </option> + ))} + </select> + </label> + )} + <label> + Column title + <input + value={title} + onChange={(e) => setTitle(e.target.value)} + placeholder="Custom title" + /> + </label> + <button className="primary" type="submit" disabled={!canSubmit}> + Add column + </button> + </form> + </div> + </div> + ); +} + +function defaultTitle(kind: DeckColumn["kind"]) { + switch (kind) { + case "following": + return "Following"; + case "bookmarks": + return "Bookmarks"; + case "list": + return "List"; + case "chat": + return "Chat"; + default: + return "For You"; + } +} |
