From 2ddc156ab57689b3fd5067c8b03293ea7e92b42b Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Thu, 16 Jan 2025 13:52:19 -0700 Subject: [PATCH 1/6] add curator username --- backend/src/services/db/schema.ts | 2 ++ .../src/services/submissions/submission.service.ts | 2 ++ backend/src/types/twitter.ts | 2 ++ frontend/src/components/FeedItem.tsx | 11 +++++++++++ frontend/src/components/Settings.tsx | 4 +--- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/src/services/db/schema.ts b/backend/src/services/db/schema.ts index cd109c2..f2e510a 100644 --- a/backend/src/services/db/schema.ts +++ b/backend/src/services/db/schema.ts @@ -42,6 +42,8 @@ export const submissions = table( tweetId: text("tweet_id").primaryKey(), userId: text("user_id").notNull(), username: text("username").notNull(), + curatorId: text("curator_id").notNull(), + curatorUsername: text("curator_username").notNull(), content: text("content").notNull(), description: text("description"), status: text("status") diff --git a/backend/src/services/submissions/submission.service.ts b/backend/src/services/submissions/submission.service.ts index 83f5451..b873d89 100644 --- a/backend/src/services/submissions/submission.service.ts +++ b/backend/src/services/submissions/submission.service.ts @@ -162,6 +162,8 @@ export class SubmissionService { tweetId: originalTweet.id!, userId: originalTweet.userId!, username: originalTweet.username!, + curatorId: userId, + curatorUsername: tweet.username!, content: originalTweet.text || "", description: this.extractDescription(tweet), status: this.config.global.defaultStatus as diff --git a/backend/src/types/twitter.ts b/backend/src/types/twitter.ts index 2517881..3e00f34 100644 --- a/backend/src/types/twitter.ts +++ b/backend/src/types/twitter.ts @@ -2,6 +2,8 @@ export interface TwitterSubmission { tweetId: string; userId: string; username: string; + curatorId: string; + curatorUsername: string; content: string; description?: string; status: "pending" | "approved" | "rejected"; diff --git a/frontend/src/components/FeedItem.tsx b/frontend/src/components/FeedItem.tsx index 20c0d55..f52d9af 100644 --- a/frontend/src/components/FeedItem.tsx +++ b/frontend/src/components/FeedItem.tsx @@ -118,6 +118,17 @@ export const FeedItem = ({ submission }: FeedItemProps) => { {submission.description && (

Curator's Notes:

+
+ Curated by{" "} + + @{submission.curatorUsername} + +

{submission.description}

)} diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx index 5a5dab6..ac1c855 100644 --- a/frontend/src/components/Settings.tsx +++ b/frontend/src/components/Settings.tsx @@ -1,9 +1,7 @@ import { useState } from "react"; -import { useLiveUpdates } from "../contexts/LiveUpdateContext"; import { useAppConfig, useUpdateLastTweetId } from "../lib/api"; export default function Settings() { - const { lastTweetId } = useLiveUpdates(); const { data: config } = useAppConfig(); const updateTweetId = useUpdateLastTweetId(); const [newTweetId, setNewTweetId] = useState(""); @@ -125,7 +123,7 @@ export default function Settings() {

Current ID:

- {lastTweetId || "Not set"} + {"Not set"}
From 9d573c1afe36af44f77eb0f8be9c8543b0b1aa57 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Thu, 16 Jan 2025 14:20:12 -0700 Subject: [PATCH 2/6] curators notes --- frontend/src/components/FeedItem.tsx | 141 +++++++++++++++++---------- frontend/src/routes/feed.$feedId.tsx | 106 ++++++++++---------- 2 files changed, 144 insertions(+), 103 deletions(-) diff --git a/frontend/src/components/FeedItem.tsx b/frontend/src/components/FeedItem.tsx index f52d9af..48e4795 100644 --- a/frontend/src/components/FeedItem.tsx +++ b/frontend/src/components/FeedItem.tsx @@ -7,6 +7,16 @@ const getTweetUrl = (tweetId: string, username: string) => { return `https://x.com/${username}/status/${tweetId}`; }; +const getTwitterIntentUrl = (tweetId: string, action: "approve" | "reject") => { + const baseUrl = "https://twitter.com/intent/tweet"; + // Add in_reply_to_status_id parameter to make it a reply + const params = new URLSearchParams({ + text: `@${BOT_ID} #${action}`, + in_reply_to: tweetId, + }); + return `${baseUrl}?${params.toString()}`; +}; + const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString(); }; @@ -32,9 +42,13 @@ interface FeedItemProps { } export const FeedItem = ({ submission }: FeedItemProps) => { + const tweetId = submission.status === "pending" + ? submission.acknowledgmentTweetId + : submission.moderationResponseTweetId; + return (
-
+
@@ -60,12 +74,35 @@ export const FeedItem = ({ submission }: FeedItemProps) => { {formatDate(submission.createdAt)}
- {(submission.status === "approved" || - submission.status === "rejected") && - submission.moderationHistory?.length > 0 && ( -
+
+

+ {submission.content} +

+
+
+ {tweetId && ( + + + + )} +
+
+ +
+
+ {(submission.status === "approved" || + submission.status === "rejected") && + ( +
+
+

Moderation Notes

+ ·
- Moderated by{" "} + by{" "} { }
- {submission.moderationHistory?.[ - submission.moderationHistory.length - 1 - ]?.note && ( -
- Moderation Note:{" "} - { - submission.moderationHistory?.[ - submission.moderationHistory.length - 1 - ]?.note - } -
- )}
- )} -
-

- {submission.content} -

-
-
- - - + {submission.moderationHistory?.[ + submission.moderationHistory.length - 1 + ]?.note && ( +

+ { + submission.moderationHistory?.[ + submission.moderationHistory.length - 1 + ]?.note + } +

+ )} +
+ )} + + {submission.status === "pending" && ( +
+
+

Curator's Notes

+ · + +
+

{submission.description}

+
+ )}
-
- {submission.description && ( -
-

Curator's Notes:

-
- Curated by{" "} + {submission.status === "pending" && submission.acknowledgmentTweetId && ( + -

{submission.description}

-
- )} + )} +
); }; diff --git a/frontend/src/routes/feed.$feedId.tsx b/frontend/src/routes/feed.$feedId.tsx index e2ca483..15ae0d8 100644 --- a/frontend/src/routes/feed.$feedId.tsx +++ b/frontend/src/routes/feed.$feedId.tsx @@ -3,6 +3,8 @@ import FeedItem from "../components/FeedItem"; import FeedList from "../components/FeedList"; import Layout from "../components/Layout"; import { useFeedConfig, useFeedItems } from "../lib/api"; +import { useState } from "react"; +import { TwitterSubmission } from "../types/twitter"; export const Route = createFileRoute("/feed/$feedId")({ component: FeedPage, @@ -12,6 +14,11 @@ function FeedPage() { const { feedId } = Route.useParams(); const { data: feed } = useFeedConfig(feedId); const { data: items = [] } = useFeedItems(feedId); + const [statusFilter, setStatusFilter] = useState<"all" | TwitterSubmission["status"]>("all"); + + const filteredItems = items.filter( + (item) => statusFilter === "all" || item.status === statusFilter + ); const sidebarContent = (
@@ -71,57 +78,6 @@ function FeedPage() {
- - {/* Commented out plugin configurations for future use - {feed.outputs.stream?.enabled && feed.outputs.stream.distribute && ( -
-
- {feed.outputs.stream.distribute.map((plugin, index) => ( -
-

{plugin.plugin}

-
-                  {JSON.stringify(plugin.config, null, 2)}
-                
-
- ))} -
-
- )} - - {feed.outputs.recap?.enabled && ( -
-
- {feed.outputs.recap.transform && ( -
-

- {feed.outputs.recap.transform.plugin} (Transform) -

-
-                  {JSON.stringify(feed.outputs.recap.transform.config, null, 2)}
-                
-
- )} - - {feed.outputs.recap.distribute?.map((plugin, index) => ( -
-

- {plugin.plugin} (Distribute) -

-
-                  {JSON.stringify(plugin.config, null, 2)}
-                
-
- ))} -
-
- )} - */} ); @@ -130,13 +86,55 @@ function FeedPage() {

{feed?.name || "Loading..."}

+
+ + + + +
- {items.length === 0 ? ( + {filteredItems.length === 0 ? (
-

No items yet

+

No items found

) : ( - items.map((item) => ) + filteredItems.map((item) => ) )}
From 23cb96ceb337fdf99513a1b1c992743f962f4fca Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Thu, 16 Jan 2025 14:54:09 -0700 Subject: [PATCH 3/6] modal --- backend/src/index.ts | 16 ++-- frontend/README.md | 1 - frontend/src/components/FeedItem.tsx | 16 ++-- frontend/src/components/FeedList.tsx | 8 +- frontend/src/components/Header.tsx | 115 ++++++++++++++----------- frontend/src/components/HowItWorks.tsx | 42 +++++++++ frontend/src/components/Layout.tsx | 2 +- frontend/src/components/Modal.tsx | 41 +++++++++ frontend/src/lib/config.ts | 8 ++ frontend/src/routeTree.gen.ts | 112 ++++++++++++------------ frontend/src/routes/feed.$feedId.tsx | 76 ++++++++-------- 11 files changed, 278 insertions(+), 159 deletions(-) create mode 100644 frontend/src/components/HowItWorks.tsx create mode 100644 frontend/src/components/Modal.tsx create mode 100644 frontend/src/lib/config.ts diff --git a/backend/src/index.ts b/backend/src/index.ts index e95c698..28d69f5 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -76,7 +76,6 @@ export async function main() { contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], - connectSrc: ["'self'", "ws:", "wss:"], // Allow WebSocket connections scriptSrc: ["'self'", "'unsafe-inline'"], // Required for some frontend frameworks styleSrc: ["'self'", "'unsafe-inline'"], // Required for styled-components imgSrc: ["'self'", "data:", "https:"], // Allow images from HTTPS sources @@ -125,26 +124,26 @@ export async function main() { : db.getAllSubmissions(); }) .get( - "/api/feed/:hashtagId", + "/api/submissions/:hashtagId", ({ params: { hashtagId } }: { params: { hashtagId: string } }) => { const config = configService.getConfig(); const feed = config.feeds.find((f) => f.id === hashtagId); if (!feed) { throw new Error(`Feed not found: ${hashtagId}`); } - + // this should be pending submissions return db.getSubmissionsByFeed(hashtagId); }, ) .get( - "/api/submissions/:hashtagId", + "/api/feed/:hashtagId", ({ params: { hashtagId } }: { params: { hashtagId: string } }) => { const config = configService.getConfig(); const feed = config.feeds.find((f) => f.id === hashtagId); if (!feed) { throw new Error(`Feed not found: ${hashtagId}`); } - // this should be pending submissions + return db.getSubmissionsByFeed(hashtagId); }, ) @@ -165,6 +164,13 @@ export async function main() { const config = configService.getConfig(); return config.feeds; }) + .get( + "/api/config", + () => { + const config = configService.getConfig(); + return config; + }, + ) .get( "/api/config/:feedId", ({ params: { feedId } }: { params: { feedId: string } }) => { diff --git a/frontend/README.md b/frontend/README.md index 575f65b..2ff9dae 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -114,7 +114,6 @@ The frontend communicates with the [backend service](../backend/README.md) throu - Submission handling via `/submit` endpoint - Content retrieval through `/submissions` -- Real-time updates using polling (future: WebSocket support) See the [Backend README](../backend/README.md) for detailed API documentation. diff --git a/frontend/src/components/FeedItem.tsx b/frontend/src/components/FeedItem.tsx index 48e4795..ccbcc5b 100644 --- a/frontend/src/components/FeedItem.tsx +++ b/frontend/src/components/FeedItem.tsx @@ -1,17 +1,16 @@ import { HiExternalLink } from "react-icons/hi"; import { TwitterSubmission } from "../types/twitter"; - -const BOT_ID = "test_curation"; +import { useBotId } from "../lib/config"; const getTweetUrl = (tweetId: string, username: string) => { return `https://x.com/${username}/status/${tweetId}`; }; -const getTwitterIntentUrl = (tweetId: string, action: "approve" | "reject") => { +const getTwitterIntentUrl = (tweetId: string, action: "approve" | "reject", botId: string) => { const baseUrl = "https://twitter.com/intent/tweet"; // Add in_reply_to_status_id parameter to make it a reply const params = new URLSearchParams({ - text: `@${BOT_ID} #${action}`, + text: `@${botId} #${action}`, in_reply_to: tweetId, }); return `${baseUrl}?${params.toString()}`; @@ -42,6 +41,7 @@ interface FeedItemProps { } export const FeedItem = ({ submission }: FeedItemProps) => { + const botId = useBotId(); const tweetId = submission.status === "pending" ? submission.acknowledgmentTweetId : submission.moderationResponseTweetId; @@ -82,7 +82,7 @@ export const FeedItem = ({ submission }: FeedItemProps) => {
{tweetId && ( @@ -96,7 +96,7 @@ export const FeedItem = ({ submission }: FeedItemProps) => {
{(submission.status === "approved" || submission.status === "rejected") && - ( + submission.moderationHistory?.length > 0 && (

Moderation Notes

@@ -157,7 +157,7 @@ export const FeedItem = ({ submission }: FeedItemProps) => { {submission.status === "pending" && submission.acknowledgmentTweetId && (
{ Approve { return (
-
+

Feeds

scroll @@ -37,15 +37,15 @@ const FeedList = () => { key={feed.id} to="/feed/$feedId" params={{ feedId: feed.id }} - className={`flex-shrink-0 min-w-[200px] mx-2 md:mx-0 md:min-w-0 block px-4 py-2 text-sm border-2 border-black shadow-sharp transition-all duration-200 md:mb-2 ${ + className={`flex-shrink-0 min-w-[200px] mx-2 md:mx-0 md:min-w-0 block px-4 py-2 text-sm border-2 border-black shadow-sharp transition-all duration-200 md:mb-2 ${ feedId === feed.id ? "bg-gray-100 text-black font-medium translate-x-0.5 translate-y-0.5 shadow-none" : "text-gray-600 hover:shadow-sharp-hover hover:-translate-x-0.5 hover:-translate-y-0.5 hover:bg-gray-50" }`} > -
+
{feed.name} - #{feed.hashtag} + #{feed.id}
))} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index b411083..347c9ea 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,57 +1,76 @@ import { FaTwitter, FaBook, FaGithub, FaTelegram } from "react-icons/fa"; import { Link } from "@tanstack/react-router"; +import { useState } from "react"; +import { Modal } from "./Modal"; +import { HowItWorks } from "./HowItWorks"; const Header = () => { + const [showHowItWorks, setShowHowItWorks] = useState(false); + return ( -
- - curate.fun Logo -
+ + setShowHowItWorks(false)}> + + + ); }; diff --git a/frontend/src/components/HowItWorks.tsx b/frontend/src/components/HowItWorks.tsx new file mode 100644 index 0000000..f9af648 --- /dev/null +++ b/frontend/src/components/HowItWorks.tsx @@ -0,0 +1,42 @@ +import { useBotId } from "../lib/config"; + +export function HowItWorks() { + const botId = useBotId(); + + return ( +
+

How It Works

+
+
+

1. Curation

+

+ Mention @{botId} with your feed's hashtag to submit content. For example: + + !submit @{botId} #ethereum Great article about web3! + +

+
+ +
+

2. Moderation

+

+ Designated approvers review submissions and can approve or reject them using hashtags: + + @{botId} #approve + + + @{botId} #reject + +

+
+ +
+

3. Distribution

+

+ Approved content is automatically distributed across configured platforms and formats. +

+
+
+
+ ); +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index c4393df..c82b2a9 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -21,7 +21,7 @@ const Layout = ({ children, sidebar, rightPanel }: LayoutProps) => {
{/* Left Sidebar - Feed List (Desktop) */}
-
{sidebar}
+
{sidebar}
{/* Main Content Area */} diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx new file mode 100644 index 0000000..bd569bb --- /dev/null +++ b/frontend/src/components/Modal.tsx @@ -0,0 +1,41 @@ +import { useEffect } from "react"; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; +} + +export function Modal({ isOpen, onClose, children }: ModalProps) { + useEffect(() => { + if (isOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "unset"; + } + return () => { + document.body.style.overflow = "unset"; + }; + }, [isOpen]); + + if (!isOpen) return null; + + return ( +
+ + ); +} diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts new file mode 100644 index 0000000..aac5f76 --- /dev/null +++ b/frontend/src/lib/config.ts @@ -0,0 +1,8 @@ +import { useAppConfig } from "./api"; + +const DEFAULT_BOT_ID = "test_curation"; + +export function useBotId() { + const { data: config } = useAppConfig(); + return config?.global?.botId || DEFAULT_BOT_ID; +} diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index dc00964..0e9f720 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -10,104 +10,104 @@ // Import Routes -import { Route as rootRoute } from "./routes/__root"; -import { Route as SettingsImport } from "./routes/settings"; -import { Route as IndexImport } from "./routes/index"; -import { Route as FeedFeedIdImport } from "./routes/feed.$feedId"; +import { Route as rootRoute } from './routes/__root' +import { Route as SettingsImport } from './routes/settings' +import { Route as IndexImport } from './routes/index' +import { Route as FeedFeedIdImport } from './routes/feed.$feedId' // Create/Update Routes const SettingsRoute = SettingsImport.update({ - id: "/settings", - path: "/settings", + id: '/settings', + path: '/settings', getParentRoute: () => rootRoute, -} as any); +} as any) const IndexRoute = IndexImport.update({ - id: "/", - path: "/", + id: '/', + path: '/', getParentRoute: () => rootRoute, -} as any); +} as any) const FeedFeedIdRoute = FeedFeedIdImport.update({ - id: "/feed/$feedId", - path: "/feed/$feedId", + id: '/feed/$feedId', + path: '/feed/$feedId', getParentRoute: () => rootRoute, -} as any); +} as any) // Populate the FileRoutesByPath interface -declare module "@tanstack/react-router" { +declare module '@tanstack/react-router' { interface FileRoutesByPath { - "/": { - id: "/"; - path: "/"; - fullPath: "/"; - preLoaderRoute: typeof IndexImport; - parentRoute: typeof rootRoute; - }; - "/settings": { - id: "/settings"; - path: "/settings"; - fullPath: "/settings"; - preLoaderRoute: typeof SettingsImport; - parentRoute: typeof rootRoute; - }; - "/feed/$feedId": { - id: "/feed/$feedId"; - path: "/feed/$feedId"; - fullPath: "/feed/$feedId"; - preLoaderRoute: typeof FeedFeedIdImport; - parentRoute: typeof rootRoute; - }; + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsImport + parentRoute: typeof rootRoute + } + '/feed/$feedId': { + id: '/feed/$feedId' + path: '/feed/$feedId' + fullPath: '/feed/$feedId' + preLoaderRoute: typeof FeedFeedIdImport + parentRoute: typeof rootRoute + } } } // Create and export the route tree export interface FileRoutesByFullPath { - "/": typeof IndexRoute; - "/settings": typeof SettingsRoute; - "/feed/$feedId": typeof FeedFeedIdRoute; + '/': typeof IndexRoute + '/settings': typeof SettingsRoute + '/feed/$feedId': typeof FeedFeedIdRoute } export interface FileRoutesByTo { - "/": typeof IndexRoute; - "/settings": typeof SettingsRoute; - "/feed/$feedId": typeof FeedFeedIdRoute; + '/': typeof IndexRoute + '/settings': typeof SettingsRoute + '/feed/$feedId': typeof FeedFeedIdRoute } export interface FileRoutesById { - __root__: typeof rootRoute; - "/": typeof IndexRoute; - "/settings": typeof SettingsRoute; - "/feed/$feedId": typeof FeedFeedIdRoute; + __root__: typeof rootRoute + '/': typeof IndexRoute + '/settings': typeof SettingsRoute + '/feed/$feedId': typeof FeedFeedIdRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath; - fullPaths: "/" | "/settings" | "/feed/$feedId"; - fileRoutesByTo: FileRoutesByTo; - to: "/" | "/settings" | "/feed/$feedId"; - id: "__root__" | "/" | "/settings" | "/feed/$feedId"; - fileRoutesById: FileRoutesById; + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/settings' | '/feed/$feedId' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/settings' | '/feed/$feedId' + id: '__root__' | '/' | '/settings' | '/feed/$feedId' + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute; - SettingsRoute: typeof SettingsRoute; - FeedFeedIdRoute: typeof FeedFeedIdRoute; + IndexRoute: typeof IndexRoute + SettingsRoute: typeof SettingsRoute + FeedFeedIdRoute: typeof FeedFeedIdRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, SettingsRoute: SettingsRoute, FeedFeedIdRoute: FeedFeedIdRoute, -}; +} export const routeTree = rootRoute ._addFileChildren(rootRouteChildren) - ._addFileTypes(); + ._addFileTypes() /* ROUTE_MANIFEST_START { diff --git a/frontend/src/routes/feed.$feedId.tsx b/frontend/src/routes/feed.$feedId.tsx index 15ae0d8..783cee7 100644 --- a/frontend/src/routes/feed.$feedId.tsx +++ b/frontend/src/routes/feed.$feedId.tsx @@ -1,30 +1,32 @@ -import { createFileRoute } from "@tanstack/react-router"; -import FeedItem from "../components/FeedItem"; -import FeedList from "../components/FeedList"; -import Layout from "../components/Layout"; -import { useFeedConfig, useFeedItems } from "../lib/api"; -import { useState } from "react"; -import { TwitterSubmission } from "../types/twitter"; +import { createFileRoute } from '@tanstack/react-router' +import FeedItem from '../components/FeedItem' +import FeedList from '../components/FeedList' +import Layout from '../components/Layout' +import { useFeedConfig, useFeedItems } from '../lib/api' +import { useState } from 'react' +import { TwitterSubmission } from '../types/twitter' -export const Route = createFileRoute("/feed/$feedId")({ +export const Route = createFileRoute('/feed/$feedId')({ component: FeedPage, -}); +}) function FeedPage() { - const { feedId } = Route.useParams(); - const { data: feed } = useFeedConfig(feedId); - const { data: items = [] } = useFeedItems(feedId); - const [statusFilter, setStatusFilter] = useState<"all" | TwitterSubmission["status"]>("all"); + const { feedId } = Route.useParams() + const { data: feed } = useFeedConfig(feedId) + const { data: items = [] } = useFeedItems(feedId) + const [statusFilter, setStatusFilter] = useState< + 'all' | TwitterSubmission['status'] + >('all') const filteredItems = items.filter( - (item) => statusFilter === "all" || item.status === statusFilter - ); + (item) => statusFilter === 'all' || item.status === statusFilter, + ) const sidebarContent = (
- ); + ) const rightPanelContent = feed && (
@@ -79,50 +81,50 @@ function FeedPage() {
- ); + ) return (
-

{feed?.name || "Loading..."}

+

{feed?.name || 'Loading...'}

) : ( - filteredItems.map((item) => ) + filteredItems.map((item) => ( + + )) )}
- ); + ) } From 1dc2149c7d2ab8a3b17acb4f758688652c02f666 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Thu, 16 Jan 2025 15:03:12 -0700 Subject: [PATCH 4/6] helpful text --- frontend/src/components/FeedList.tsx | 52 ++++++++++++++++------------ frontend/src/routes/__root.tsx | 11 +++++- frontend/src/routes/feed.$feedId.tsx | 5 ++- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx index 44ff030..b3db2bd 100644 --- a/frontend/src/components/FeedList.tsx +++ b/frontend/src/components/FeedList.tsx @@ -24,31 +24,39 @@ const FeedList = () => { return (
-
+

Feeds

- - scroll - - + {feeds.length > 0 && ( + + scroll + + + )}
); diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index d07d4aa..93b0d80 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,6 +1,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Outlet, createRootRoute } from "@tanstack/react-router"; -import { TanStackRouterDevtools } from "@tanstack/router-devtools"; +import React from "react"; const queryClient = new QueryClient(); @@ -8,6 +8,15 @@ export const Route = createRootRoute({ component: RootComponent, }); +export const TanStackRouterDevtools = + process.env.NODE_ENV === "production" + ? () => null + : React.lazy(() => + import("@tanstack/router-devtools").then((res) => ({ + default: res.TanStackRouterDevtools, + })), + ); + function RootComponent() { return ( <> diff --git a/frontend/src/routes/feed.$feedId.tsx b/frontend/src/routes/feed.$feedId.tsx index 783cee7..7898a55 100644 --- a/frontend/src/routes/feed.$feedId.tsx +++ b/frontend/src/routes/feed.$feedId.tsx @@ -5,6 +5,7 @@ import Layout from '../components/Layout' import { useFeedConfig, useFeedItems } from '../lib/api' import { useState } from 'react' import { TwitterSubmission } from '../types/twitter' +import { useBotId } from '../lib/config' export const Route = createFileRoute('/feed/$feedId')({ component: FeedPage, @@ -14,6 +15,7 @@ function FeedPage() { const { feedId } = Route.useParams() const { data: feed } = useFeedConfig(feedId) const { data: items = [] } = useFeedItems(feedId) + const botId = useBotId(); const [statusFilter, setStatusFilter] = useState< 'all' | TwitterSubmission['status'] >('all') @@ -132,8 +134,9 @@ function FeedPage() {
{filteredItems.length === 0 ? ( -
+

No items found

+

comment with "!submit @{botId} #{feed?.id}" to start curating

) : ( filteredItems.map((item) => ( From 121dab62c975e970697afb939c26d329b274c817 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Thu, 16 Jan 2025 15:04:30 -0700 Subject: [PATCH 5/6] fmt --- backend/src/index.ts | 11 +- .../submissions/submission.service.ts | 2 +- frontend/src/components/FeedItem.tsx | 68 +++++++---- frontend/src/components/HowItWorks.tsx | 9 +- frontend/src/routeTree.gen.ts | 112 +++++++++--------- frontend/src/routes/feed.$feedId.tsx | 78 ++++++------ 6 files changed, 149 insertions(+), 131 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 28d69f5..db97662 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -164,13 +164,10 @@ export async function main() { const config = configService.getConfig(); return config.feeds; }) - .get( - "/api/config", - () => { - const config = configService.getConfig(); - return config; - }, - ) + .get("/api/config", () => { + const config = configService.getConfig(); + return config; + }) .get( "/api/config/:feedId", ({ params: { feedId } }: { params: { feedId: string } }) => { diff --git a/backend/src/services/submissions/submission.service.ts b/backend/src/services/submissions/submission.service.ts index b873d89..5cd9055 100644 --- a/backend/src/services/submissions/submission.service.ts +++ b/backend/src/services/submissions/submission.service.ts @@ -15,7 +15,7 @@ export class SubmissionService { private readonly twitterService: TwitterService, private readonly DistributionService: DistributionService, private readonly config: AppConfig, - ) { } + ) {} async initialize(): Promise { // Initialize feeds and admin cache from config diff --git a/frontend/src/components/FeedItem.tsx b/frontend/src/components/FeedItem.tsx index ccbcc5b..feecc65 100644 --- a/frontend/src/components/FeedItem.tsx +++ b/frontend/src/components/FeedItem.tsx @@ -6,7 +6,11 @@ const getTweetUrl = (tweetId: string, username: string) => { return `https://x.com/${username}/status/${tweetId}`; }; -const getTwitterIntentUrl = (tweetId: string, action: "approve" | "reject", botId: string) => { +const getTwitterIntentUrl = ( + tweetId: string, + action: "approve" | "reject", + botId: string, +) => { const baseUrl = "https://twitter.com/intent/tweet"; // Add in_reply_to_status_id parameter to make it a reply const params = new URLSearchParams({ @@ -42,9 +46,10 @@ interface FeedItemProps { export const FeedItem = ({ submission }: FeedItemProps) => { const botId = useBotId(); - const tweetId = submission.status === "pending" - ? submission.acknowledgmentTweetId - : submission.moderationResponseTweetId; + const tweetId = + submission.status === "pending" + ? submission.acknowledgmentTweetId + : submission.moderationResponseTweetId; return (
@@ -130,7 +135,7 @@ export const FeedItem = ({ submission }: FeedItemProps) => {

)}
- )} + )} {submission.status === "pending" && (
@@ -149,31 +154,42 @@ export const FeedItem = ({ submission }: FeedItemProps) => {
-

{submission.description}

+

+ {submission.description} +

)}
- {submission.status === "pending" && submission.acknowledgmentTweetId && ( - - )} + {submission.status === "pending" && + submission.acknowledgmentTweetId && ( + + )}
); diff --git a/frontend/src/components/HowItWorks.tsx b/frontend/src/components/HowItWorks.tsx index f9af648..4c8d671 100644 --- a/frontend/src/components/HowItWorks.tsx +++ b/frontend/src/components/HowItWorks.tsx @@ -10,7 +10,8 @@ export function HowItWorks() {

1. Curation

- Mention @{botId} with your feed's hashtag to submit content. For example: + Mention @{botId} with your feed's hashtag to submit content. For + example: !submit @{botId} #ethereum Great article about web3! @@ -20,7 +21,8 @@ export function HowItWorks() {

2. Moderation

- Designated approvers review submissions and can approve or reject them using hashtags: + Designated approvers review submissions and can approve or reject + them using hashtags: @{botId} #approve @@ -33,7 +35,8 @@ export function HowItWorks() {

3. Distribution

- Approved content is automatically distributed across configured platforms and formats. + Approved content is automatically distributed across configured + platforms and formats.

diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 0e9f720..dc00964 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -10,104 +10,104 @@ // Import Routes -import { Route as rootRoute } from './routes/__root' -import { Route as SettingsImport } from './routes/settings' -import { Route as IndexImport } from './routes/index' -import { Route as FeedFeedIdImport } from './routes/feed.$feedId' +import { Route as rootRoute } from "./routes/__root"; +import { Route as SettingsImport } from "./routes/settings"; +import { Route as IndexImport } from "./routes/index"; +import { Route as FeedFeedIdImport } from "./routes/feed.$feedId"; // Create/Update Routes const SettingsRoute = SettingsImport.update({ - id: '/settings', - path: '/settings', + id: "/settings", + path: "/settings", getParentRoute: () => rootRoute, -} as any) +} as any); const IndexRoute = IndexImport.update({ - id: '/', - path: '/', + id: "/", + path: "/", getParentRoute: () => rootRoute, -} as any) +} as any); const FeedFeedIdRoute = FeedFeedIdImport.update({ - id: '/feed/$feedId', - path: '/feed/$feedId', + id: "/feed/$feedId", + path: "/feed/$feedId", getParentRoute: () => rootRoute, -} as any) +} as any); // Populate the FileRoutesByPath interface -declare module '@tanstack/react-router' { +declare module "@tanstack/react-router" { interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexImport - parentRoute: typeof rootRoute - } - '/settings': { - id: '/settings' - path: '/settings' - fullPath: '/settings' - preLoaderRoute: typeof SettingsImport - parentRoute: typeof rootRoute - } - '/feed/$feedId': { - id: '/feed/$feedId' - path: '/feed/$feedId' - fullPath: '/feed/$feedId' - preLoaderRoute: typeof FeedFeedIdImport - parentRoute: typeof rootRoute - } + "/": { + id: "/"; + path: "/"; + fullPath: "/"; + preLoaderRoute: typeof IndexImport; + parentRoute: typeof rootRoute; + }; + "/settings": { + id: "/settings"; + path: "/settings"; + fullPath: "/settings"; + preLoaderRoute: typeof SettingsImport; + parentRoute: typeof rootRoute; + }; + "/feed/$feedId": { + id: "/feed/$feedId"; + path: "/feed/$feedId"; + fullPath: "/feed/$feedId"; + preLoaderRoute: typeof FeedFeedIdImport; + parentRoute: typeof rootRoute; + }; } } // Create and export the route tree export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '/settings': typeof SettingsRoute - '/feed/$feedId': typeof FeedFeedIdRoute + "/": typeof IndexRoute; + "/settings": typeof SettingsRoute; + "/feed/$feedId": typeof FeedFeedIdRoute; } export interface FileRoutesByTo { - '/': typeof IndexRoute - '/settings': typeof SettingsRoute - '/feed/$feedId': typeof FeedFeedIdRoute + "/": typeof IndexRoute; + "/settings": typeof SettingsRoute; + "/feed/$feedId": typeof FeedFeedIdRoute; } export interface FileRoutesById { - __root__: typeof rootRoute - '/': typeof IndexRoute - '/settings': typeof SettingsRoute - '/feed/$feedId': typeof FeedFeedIdRoute + __root__: typeof rootRoute; + "/": typeof IndexRoute; + "/settings": typeof SettingsRoute; + "/feed/$feedId": typeof FeedFeedIdRoute; } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/settings' | '/feed/$feedId' - fileRoutesByTo: FileRoutesByTo - to: '/' | '/settings' | '/feed/$feedId' - id: '__root__' | '/' | '/settings' | '/feed/$feedId' - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath; + fullPaths: "/" | "/settings" | "/feed/$feedId"; + fileRoutesByTo: FileRoutesByTo; + to: "/" | "/settings" | "/feed/$feedId"; + id: "__root__" | "/" | "/settings" | "/feed/$feedId"; + fileRoutesById: FileRoutesById; } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - SettingsRoute: typeof SettingsRoute - FeedFeedIdRoute: typeof FeedFeedIdRoute + IndexRoute: typeof IndexRoute; + SettingsRoute: typeof SettingsRoute; + FeedFeedIdRoute: typeof FeedFeedIdRoute; } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, SettingsRoute: SettingsRoute, FeedFeedIdRoute: FeedFeedIdRoute, -} +}; export const routeTree = rootRoute ._addFileChildren(rootRouteChildren) - ._addFileTypes() + ._addFileTypes(); /* ROUTE_MANIFEST_START { diff --git a/frontend/src/routes/feed.$feedId.tsx b/frontend/src/routes/feed.$feedId.tsx index 7898a55..2a24f85 100644 --- a/frontend/src/routes/feed.$feedId.tsx +++ b/frontend/src/routes/feed.$feedId.tsx @@ -1,34 +1,34 @@ -import { createFileRoute } from '@tanstack/react-router' -import FeedItem from '../components/FeedItem' -import FeedList from '../components/FeedList' -import Layout from '../components/Layout' -import { useFeedConfig, useFeedItems } from '../lib/api' -import { useState } from 'react' -import { TwitterSubmission } from '../types/twitter' -import { useBotId } from '../lib/config' +import { createFileRoute } from "@tanstack/react-router"; +import FeedItem from "../components/FeedItem"; +import FeedList from "../components/FeedList"; +import Layout from "../components/Layout"; +import { useFeedConfig, useFeedItems } from "../lib/api"; +import { useState } from "react"; +import { TwitterSubmission } from "../types/twitter"; +import { useBotId } from "../lib/config"; -export const Route = createFileRoute('/feed/$feedId')({ +export const Route = createFileRoute("/feed/$feedId")({ component: FeedPage, -}) +}); function FeedPage() { - const { feedId } = Route.useParams() - const { data: feed } = useFeedConfig(feedId) - const { data: items = [] } = useFeedItems(feedId) + const { feedId } = Route.useParams(); + const { data: feed } = useFeedConfig(feedId); + const { data: items = [] } = useFeedItems(feedId); const botId = useBotId(); const [statusFilter, setStatusFilter] = useState< - 'all' | TwitterSubmission['status'] - >('all') + "all" | TwitterSubmission["status"] + >("all"); const filteredItems = items.filter( - (item) => statusFilter === 'all' || item.status === statusFilter, - ) + (item) => statusFilter === "all" || item.status === statusFilter, + ); const sidebarContent = (
- ) + ); const rightPanelContent = feed && (
@@ -83,50 +83,50 @@ function FeedPage() {
- ) + ); return (
-

{feed?.name || 'Loading...'}

+

{feed?.name || "Loading..."}

- ) + ); } From 03c7f3450bfa20f733bbca922a334a1e83050d1e Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Thu, 16 Jan 2025 15:07:01 -0700 Subject: [PATCH 6/6] retry twitter 3 times --- backend/src/services/twitter/client.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/backend/src/services/twitter/client.ts b/backend/src/services/twitter/client.ts index 9f6fa3f..a5a48f0 100644 --- a/backend/src/services/twitter/client.ts +++ b/backend/src/services/twitter/client.ts @@ -43,9 +43,9 @@ export class TwitterService { // Load last checked tweet ID from cache this.lastCheckedTweetId = db.getTwitterCacheValue("last_tweet_id"); - // Try to login with retries + // Try to login with max 2 retries logger.info("Attempting Twitter login..."); - while (true) { + for (let attempt = 0; attempt < 3; attempt++) { try { await this.client.login( this.config.username, @@ -71,18 +71,24 @@ export class TwitterService { | undefined, })); db.setTwitterCookies(this.config.username, formattedCookies); + logger.info("Successfully logged in to Twitter"); break; } } catch (error) { - logger.error("Failed to login to Twitter, retrying...", error); - break; + logger.error( + `Failed to login to Twitter (attempt ${attempt + 1}/3)...`, + error, + ); } - // Wait before retrying - await new Promise((resolve) => setTimeout(resolve, 2000)); + if (attempt < 2) { + // Wait before retrying + await new Promise((resolve) => setTimeout(resolve, 2000)); + } } - logger.info("Successfully logged in to Twitter"); + // If we get here without breaking, all attempts failed + throw new Error("Failed to login to Twitter after 3 attempts"); } catch (error) { logger.error("Failed to initialize Twitter client:", error); throw error;