summaryrefslogtreecommitdiff
path: root/packages/tweetdeck/src/components/AddColumnModal.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/tweetdeck/src/components/AddColumnModal.tsx')
-rw-r--r--packages/tweetdeck/src/components/AddColumnModal.tsx234
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";
+ }
+}