summaryrefslogtreecommitdiff
path: root/packages/tweetdeck/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/tweetdeck/src')
-rw-r--r--packages/tweetdeck/src/App.tsx311
-rw-r--r--packages/tweetdeck/src/Test.tsx19
-rw-r--r--packages/tweetdeck/src/components/TweetCard.tsx2
-rw-r--r--packages/tweetdeck/src/index.ts3
-rw-r--r--packages/tweetdeck/src/pages/Deck.tsx310
5 files changed, 340 insertions, 305 deletions
diff --git a/packages/tweetdeck/src/App.tsx b/packages/tweetdeck/src/App.tsx
index 924ff9a..44b6405 100644
--- a/packages/tweetdeck/src/App.tsx
+++ b/packages/tweetdeck/src/App.tsx
@@ -1,310 +1,15 @@
-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";
+import "./styles/normalize.css";
+import { Toaster } from "react-hot-toast";
+import Deck from "./pages/Deck";
+import Test from "./Test";
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>
+ <>
+ <Test />
+ <Toaster position="top-center" />
+ </>
);
}
-
-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;
diff --git a/packages/tweetdeck/src/Test.tsx b/packages/tweetdeck/src/Test.tsx
new file mode 100644
index 0000000..28de9b9
--- /dev/null
+++ b/packages/tweetdeck/src/Test.tsx
@@ -0,0 +1,19 @@
+import "./styles/normalize.css";
+import "./styles/index.css";
+import { LangText } from "@sortug/prosody-ui";
+import toast from "react-hot-toast";
+
+export function Test() {
+ const text = `อุตุฯ ฉบับ 16 เช็กจังหวัดภาคใต้เจอฝนตกหนักถึงหนักมาก`;
+ return (
+ <div className="app-shell">
+ <LangText
+ lang="th"
+ text={text}
+ theme="dark"
+ handleError={(e) => toast.error(e)}
+ />
+ </div>
+ );
+}
+export default Test;
diff --git a/packages/tweetdeck/src/components/TweetCard.tsx b/packages/tweetdeck/src/components/TweetCard.tsx
index 7cd2936..c9e6219 100644
--- a/packages/tweetdeck/src/components/TweetCard.tsx
+++ b/packages/tweetdeck/src/components/TweetCard.tsx
@@ -2,7 +2,7 @@ import { useCallback, useState } from "react";
import { Bookmark, Heart, Link2, MessageCircle, Repeat2 } from "lucide-react";
import type { Tweet } from "../lib/fetching/types";
import { timeAgo } from "../lib/utils/time";
-import { LangText } from "prosody-ui";
+import { LangText } from "@sortug/prosody-ui";
interface TweetCardProps {
tweet: Tweet;
diff --git a/packages/tweetdeck/src/index.ts b/packages/tweetdeck/src/index.ts
index ccc86e7..9daa973 100644
--- a/packages/tweetdeck/src/index.ts
+++ b/packages/tweetdeck/src/index.ts
@@ -1,3 +1,4 @@
+import { handler } from "@sortug/sorlang-db";
import { serve } from "bun";
import index from "./index.html";
import { TwitterApiService } from "./lib/fetching/twitter-api";
@@ -39,7 +40,7 @@ const server = serve({
routes: {
// Serve index.html for all unmatched routes.
"/*": index,
-
+ "/api/db": handler,
"/api/hello": {
async GET(req) {
return Response.json({
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;