From ff3078e93411c3467d797258744a7f17a7dbdf0a Mon Sep 17 00:00:00 2001 From: polwex Date: Wed, 16 Jul 2025 10:07:06 +0700 Subject: m --- app/src/components/bookmark-fetcher.tsx | 143 +++++++++++++++++++++ app/src/components/bookmark-list.tsx | 109 ++++++++++++++++ app/src/components/cat/Entry.tsx | 214 ++++++++++++++++++++++++++++++++ app/src/components/cat/Form.tsx | 5 + app/src/components/counter.tsx | 21 ++++ app/src/components/footer.tsx | 18 +++ app/src/components/header.tsx | 9 ++ 7 files changed, 519 insertions(+) create mode 100644 app/src/components/bookmark-fetcher.tsx create mode 100644 app/src/components/bookmark-list.tsx create mode 100644 app/src/components/cat/Entry.tsx create mode 100644 app/src/components/cat/Form.tsx create mode 100644 app/src/components/counter.tsx create mode 100644 app/src/components/footer.tsx create mode 100644 app/src/components/header.tsx (limited to 'app/src/components') diff --git a/app/src/components/bookmark-fetcher.tsx b/app/src/components/bookmark-fetcher.tsx new file mode 100644 index 0000000..6522310 --- /dev/null +++ b/app/src/components/bookmark-fetcher.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { TwitterBookmark } from '../lib/bookmark-models'; +import { BookmarkStorageService } from '../lib/bookmark-storage'; +import { BookmarkList } from './bookmark-list'; + +export function BookmarkFetcher() { + const [bookmarks, setBookmarks] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [username, setUsername] = useState(''); + const [authToken, setAuthToken] = useState(''); + + // Load existing bookmarks on mount + useEffect(() => { + const existingBookmarks = BookmarkStorageService.getBookmarks(); + setBookmarks(existingBookmarks); + }, []); + + const handleFetchBookmarks = async () => { + if (!username || !authToken) { + setError('Please enter your Twitter username and auth token'); + return; + } + + setLoading(true); + setError(null); + + try { + // Simple fetch to our API endpoint + const response = await fetch('/api/sync-bookmarks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, authToken }), + }); + + const result = await response.json(); + + if (!result.success) { + throw new Error(result.error || 'Failed to fetch bookmarks'); + } + + // Load the newly saved bookmarks + const updatedBookmarks = BookmarkStorageService.getBookmarks(); + setBookmarks(updatedBookmarks); + + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch bookmarks'); + } finally { + setLoading(false); + } + }; + + const handleClearBookmarks = () => { + BookmarkStorageService.clearAll(); + setBookmarks([]); + }; + + return ( +
+ {/* Input fields */} +
+

Twitter Credentials

+ +
+
+ + setUsername(e.target.value)} + placeholder="your_twitter_username" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ +
+ + setAuthToken(e.target.value)} + placeholder="your_auth_token" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+
+ + {/* Action buttons */} +
+ + + {bookmarks.length > 0 && ( + + )} +
+ + {/* Error message */} + {error && ( +
+ {error} +
+ )} + + {/* Bookmark list */} +
+

Your Bookmarks

+ +
+
+ ); +} \ No newline at end of file diff --git a/app/src/components/bookmark-list.tsx b/app/src/components/bookmark-list.tsx new file mode 100644 index 0000000..6138e31 --- /dev/null +++ b/app/src/components/bookmark-list.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useState } from "react"; + +import { BookmarkStorageService } from "../lib/bookmark-storage"; +import { TwitterBookmark } from "../lib/twitter-api"; + +interface BookmarkListProps { + bookmarks: TwitterBookmark[]; +} + +function formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +function truncateText(text: string, maxLength: number = 200): string { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength) + "..."; +} + +export function BookmarkList({ bookmarks }: BookmarkListProps) { + if (bookmarks.length === 0) { + return ( +
+ No bookmarks found. Click "Fetch Twitter Bookmarks" to load your + bookmarks. +
+ ); + } + + return ( +
+
+ Found {bookmarks.length} bookmark{bookmarks.length !== 1 ? "s" : ""} +
+ + {bookmarks.map((bookmark) => ( + + ))} +
+ ); +} + +function BookmarkEntry({ bookmark }: { bookmark: TwitterBookmark }) { + console.log({ bookmark }); + return ( +
+
+
+
+ + @{bookmark.author.username} + + + + {formatDate(bookmark.createdAt)} + +
+
+ {truncateText(bookmark.text)} +
+
+ {bookmark.media.pics.map((p) => ( + + ))} +
+ {bookmark.media.video.url && ( +
+
+
+ ); +} diff --git a/app/src/components/cat/Entry.tsx b/app/src/components/cat/Entry.tsx new file mode 100644 index 0000000..1ded145 --- /dev/null +++ b/app/src/components/cat/Entry.tsx @@ -0,0 +1,214 @@ +import { + CategorizationResponse, + userCategories, +} from "../../lib/categorization"; +import { TwitterApiService, TwitterBookmark } from "../../lib/twitter-api"; + +export default function ({ + bookmark, + categorization, + currentIndex, + totalCount, +}: { + bookmark: TwitterBookmark; + categorization: CategorizationResponse; + currentIndex: number; + totalCount: number; +}) { + async function processEntry() { + "use server"; + const cookie = Bun.env.TWATTER_COKI; + const api = new TwitterApiService(cookie!); + await api.removeBookmark(bookmark.id); + } + + return ( +
+ {/* Bookmark Content */} +
+
+ {bookmark.author.name} +
+

+ {bookmark.author.name} +

+

@{bookmark.author.username}

+

+ {new Date(bookmark.createdAt).toLocaleDateString()} +

+
+
+ +
+

{bookmark.text}

+
+ + {bookmark.media.pics.length > 0 && ( +
+

Images

+
+ {bookmark.media.pics.map((url, index) => ( + {`Bookmark + ))} +
+
+ )} + +
+ {bookmark.hashtags.map((tag) => ( + + #{tag} + + ))} +
+
+ + {/* LLM Analysis */} +
+

AI Analysis

+

{categorization.summary}

+ +
+

Key Topics

+
+ {categorization.keyTopics.map((topic) => ( + + {topic} + + ))} +
+
+ +
+

Suggested Categories

+ {categorization.suggestedCategories.map((suggestion, index) => ( +
+
+ + {suggestion.categories.join(", ")} + + + {(suggestion.confidence * 100).toFixed(0)}% + +
+

{suggestion.reasoning}

+
+ ))} +
+
+ + {/* Category Selection */} +
+
+

Select Categories

+ +
+

User Categories

+
+ {userCategories.map((category) => ( + + ))} +
+
+ + {categorization.newCategories.length > 0 && ( +
+

+ New Category Suggestions +

+
+ {categorization.newCategories.map((category) => ( + + ))} +
+
+ )} + +
+ + +
+ + + + +
+ {currentIndex > 0 && ( + + Previous + + )} + + {currentIndex + 1 < totalCount && ( + + Skip + + )} +
+
+
+
+ ); +} diff --git a/app/src/components/cat/Form.tsx b/app/src/components/cat/Form.tsx new file mode 100644 index 0000000..7e58be4 --- /dev/null +++ b/app/src/components/cat/Form.tsx @@ -0,0 +1,5 @@ +"use client"; + +export default function ProcessEntry() { + return
; +} diff --git a/app/src/components/counter.tsx b/app/src/components/counter.tsx new file mode 100644 index 0000000..0e540b8 --- /dev/null +++ b/app/src/components/counter.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { useState } from 'react'; + +export const Counter = () => { + const [count, setCount] = useState(0); + + const handleIncrement = () => setCount((c) => c + 1); + + return ( +
+
Count: {count}
+ +
+ ); +}; diff --git a/app/src/components/footer.tsx b/app/src/components/footer.tsx new file mode 100644 index 0000000..d9d2511 --- /dev/null +++ b/app/src/components/footer.tsx @@ -0,0 +1,18 @@ +export const Footer = () => { + return ( + + ); +}; diff --git a/app/src/components/header.tsx b/app/src/components/header.tsx new file mode 100644 index 0000000..390043b --- /dev/null +++ b/app/src/components/header.tsx @@ -0,0 +1,9 @@ +import { Link } from "waku"; + +export const Header = () => { + return ( +
+

+
+ ); +}; -- cgit v1.2.3