From d6ea00fb4cf6bab130d604037a7c03d0bbc00eef Mon Sep 17 00:00:00 2001 From: Zeeshan Ahmad Date: Tue, 25 Mar 2025 08:25:57 +0500 Subject: [PATCH 01/90] Get basic working (#109) * Get basic working * adds rss feeds * added stablewatch founder and stablecoin intern from messaria as approvers on stablecoins feed * fixes to config for grants, sui, and telegram channels (#102) * add query param for selective processing (#103) * adds query param to process * add query param for processing * simplify * add tags * Feat: implement frontend leaderboard (#93) * feat: implement frontend leaderboard * feat: implement a leaderboard in frontend * feat: implemented leaderboard * fix: rebuild implement leaderboard * fix: prettier * fix: prettier * fix: reimplement frontend leaderboard * fix: implement frontend leaderboard * approval rate * sets approval rate and hides curator --------- Co-authored-by: Elliot Braem * remove tailwind-scrollbar * added bob to desci feed * Get basic working * set .env.example * nitpicks --------- Co-authored-by: Elliot Braem Co-authored-by: codingshot <45281667+codingshot@users.noreply.github.com> Co-authored-by: Elliot Braem Co-authored-by: Louis <112561517+louisdevzz@users.noreply.github.com> --- frontend/.env.example | 1 + frontend/index.html | 18 ++-- frontend/package.json | 5 ++ frontend/rsbuild.config.ts | 3 +- frontend/src/App.tsx | 8 +- frontend/src/components/Header.tsx | 24 ++++++ frontend/src/contexts/web3auth.tsx | 125 ++++++++++++++++++++++++++++ frontend/src/hooks/use-web3-auth.ts | 12 +++ frontend/src/types/web3auth.ts | 11 +++ 9 files changed, 196 insertions(+), 11 deletions(-) create mode 100644 frontend/.env.example create mode 100644 frontend/src/contexts/web3auth.tsx create mode 100644 frontend/src/hooks/use-web3-auth.ts create mode 100644 frontend/src/types/web3auth.ts diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 00000000..c8d03e7b --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +PUBLIC_WEB3_CLIENT_ID=your_web3_client_id \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 95c2a95e..4b80d3b2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,6 +1,14 @@ + + + curate.fun - crowdsource automated content @@ -25,14 +33,8 @@ name="twitter:description" content="Curate news directly on socials and turn feeds into regular content." /> - - + + diff --git a/frontend/package.json b/frontend/package.json index bdb3b83a..c2bc8aa1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,13 +10,18 @@ "lint": "eslint ." }, "dependencies": { + "@rsbuild/plugin-node-polyfill": "^1.3.0", "@tailwindcss/typography": "^0.5.15", "@tanstack/react-query": "^5.64.1", "@tanstack/react-router": "^1.114.15", + "@web3auth/base": "^9.7.0", + "@web3auth/ethereum-provider": "^9.7.0", + "@web3auth/modal": "^9.7.0", "autoprefixer": "^10.4.20", "axios": "^1.7.9", "lucide-react": "^0.479.0", "postcss": "^8.4.49", + "process": "^0.11.10", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.4.0", diff --git a/frontend/rsbuild.config.ts b/frontend/rsbuild.config.ts index c8dd003d..eaba4da9 100644 --- a/frontend/rsbuild.config.ts +++ b/frontend/rsbuild.config.ts @@ -1,9 +1,10 @@ import { defineConfig } from "@rsbuild/core"; import { pluginReact } from "@rsbuild/plugin-react"; import { TanStackRouterRspack } from "@tanstack/router-plugin/rspack"; +import { pluginNodePolyfill } from "@rsbuild/plugin-node-polyfill"; export default defineConfig({ - plugins: [pluginReact()], + plugins: [pluginReact(), pluginNodePolyfill()], html: { template: "./index.html", }, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 708dc4c5..8b9bca0e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { createRouter, RouterProvider } from "@tanstack/react-router"; - import { routeTree } from "./routeTree.gen"; +import { Web3AuthProvider } from "./contexts/web3auth"; // Set up a Router instance const router = createRouter({ @@ -16,7 +16,11 @@ declare module "@tanstack/react-router" { } function App() { - return ; + return ( + + + + ); } export default App; diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 42e5341e..acf08685 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -3,10 +3,13 @@ import { Link } from "@tanstack/react-router"; import { useState } from "react"; import { Modal } from "./Modal"; import { HowItWorks } from "./HowItWorks"; +import { useWeb3Auth } from "../hooks/use-web3-auth"; const Header = () => { const [showHowItWorks, setShowHowItWorks] = useState(false); + const { isInitialized, isLoggedIn, login, logout } = useWeb3Auth(); + return ( <>
@@ -45,6 +48,27 @@ const Header = () => { )} +
+ {isInitialized ? ( + isLoggedIn ? ( + + ) : ( + + ) + ) : ( +

Loading...

+ )} +
+ )} + +
+ {isInitialized ? ( + isLoggedIn ? ( + + ) : ( + + ) + ) : ( +

Loading...

+ )} +
+ setShowHowItWorks(false)}> diff --git a/frontend/src/components/InfiniteFeed.tsx b/frontend/src/components/InfiniteFeed.tsx index ef3bdd13..e12b54e9 100644 --- a/frontend/src/components/InfiniteFeed.tsx +++ b/frontend/src/components/InfiniteFeed.tsx @@ -10,6 +10,7 @@ interface InfiniteFeedProps { loadingMessage?: string; noMoreItemsMessage?: string; initialLoadingMessage?: string; + showAll?: boolean; // New prop to control showing all or limited items } function InfiniteFeed({ @@ -22,6 +23,7 @@ function InfiniteFeed({ loadingMessage = "Loading more items...", noMoreItemsMessage = "No more items to load", initialLoadingMessage = "Loading items...", + showAll = true, // Default to showing all items }: InfiniteFeedProps) { // Create an intersection observer to detect when user scrolls to bottom const observerTarget = useRef(null); @@ -37,6 +39,9 @@ function InfiniteFeed({ ); useEffect(() => { + // Only set up the observer if we're showing all items + if (!showAll) return; + const element = observerTarget.current; if (!element) return; @@ -51,24 +56,29 @@ function InfiniteFeed({ observer.unobserve(element); observer.disconnect(); }; - }, [handleObserver]); + }, [handleObserver, showAll]); + + // Determine which items to render + const itemsToRender = showAll ? items : items.slice(0, 3); return ( -
- {renderItems(items)} +
+ {renderItems(itemsToRender)} - {/* Loading indicator and observer target */} -
- {isFetchingNextPage && ( -
{loadingMessage}
- )} - {!hasNextPage && items.length > 0 && !isFetchingNextPage && ( -
{noMoreItemsMessage}
- )} - {status === "pending" && items.length === 0 && ( -
{initialLoadingMessage}
- )} -
+ {/* Loading indicator and observer target - only shown when showAll is true */} + {showAll && ( +
+ {isFetchingNextPage && ( +
{loadingMessage}
+ )} + {!hasNextPage && items.length > 0 && !isFetchingNextPage && ( +
{noMoreItemsMessage}
+ )} + {status === "pending" && items.length === 0 && ( +
{initialLoadingMessage}
+ )} +
+ )}
); } diff --git a/frontend/src/components/RecentLaunches.tsx b/frontend/src/components/RecentLaunches.tsx new file mode 100644 index 00000000..e1e09291 --- /dev/null +++ b/frontend/src/components/RecentLaunches.tsx @@ -0,0 +1,128 @@ +// Token data type definition +type Token = { + id: string; + name: string; + icon: string; + price: string; + priceChange: string; +}; + +// Sample demo data +const demoTokens: Token[] = [ + { + id: "1", + name: "CGWIRE", + icon: "🔲", + price: "$0.06712", + priceChange: "+31.79%", + }, + { + id: "2", + name: "NEARW", + icon: "🐻", + price: "$0.06712", + priceChange: "+31.79%", + }, + { + id: "3", + name: "NOUNS", + icon: "👓", + price: "$0.06712", + priceChange: "+31.79%", + }, + { + id: "4", + name: "CELO", + icon: "🔶", + price: "$0.06712", + priceChange: "+31.79%", + }, + { + id: "5", + name: "ReFID", + icon: "🔵", + price: "$0.06712", + priceChange: "+31.79%", + }, + { + id: "6", + name: "ELIZA", + icon: "🟦", + price: "$0.06712", + priceChange: "+31.79%", + }, +]; + +// Arrow up icon component +const ArrowUpIcon = () => ( + + + + +); + +const TokenCard = ({ token }: { token: Token }) => { + return ( +
+
+ {token.icon} +
+
+
{token.name}
+
{token.price}
+
+
+ + {token.priceChange} +
+
+ ); +}; + +const RecentTokenLaunches = () => { + // In a real implementation, you would fetch data from an API + + // Uncomment and modify this to use with your actual API + // useEffect(() => { + // const fetchTokens = async () => { + // try { + // const response = await fetch('https://your-api-endpoint.com/tokens'); + // const data = await response.json(); + // setTokens(data); + // } catch (error) { + // console.error('Error fetching token data:', error); + // // Keep demo data as fallback + // } + // }; + // fetchTokens(); + // }, []); + + return ( +
+

Recent Token Launches

+
+ {demoTokens.map((token, index) => ( +
0 ? "mt-3 md:mt-0" : ""}`} + > + +
+ ))} +
+
+ ); +}; + +export default RecentTokenLaunches; diff --git a/frontend/src/components/Sort.tsx b/frontend/src/components/Sort.tsx new file mode 100644 index 00000000..bcf567d6 --- /dev/null +++ b/frontend/src/components/Sort.tsx @@ -0,0 +1,81 @@ +"use client"; + +import * as React from "react"; +import { Check, ChevronsUpDown } from "lucide-react"; + +import { cn } from "../lib/utils"; +import { Button } from "./ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, +} from "./ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; + +const frameworks = [ + { + value: "Most Recent", + label: "Most Recent", + }, + { + value: "A to Z", + label: "A to Z", + }, + { + value: "Z to A", + label: "Z to A", + }, +]; + +export function Sort() { + const [open, setOpen] = React.useState(false); + const [value, setValue] = React.useState(""); + + return ( + + + + + + + {/* */} + + No framework found. + + {frameworks.map((framework) => ( + { + setValue(currentValue === value ? "" : currentValue); + setOpen(false); + }} + > + + {framework.label} + + ))} + + + + + + ); +} diff --git a/frontend/src/components/StatusFilter.tsx b/frontend/src/components/StatusFilter.tsx new file mode 100644 index 00000000..a82838e9 --- /dev/null +++ b/frontend/src/components/StatusFilter.tsx @@ -0,0 +1,93 @@ +"use client"; + +import * as React from "react"; +import { Check, ChevronsUpDown } from "lucide-react"; + +import { cn } from "../lib/utils"; +import { Button } from "./ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, +} from "./ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; +import { StatusFilterType, useFilterStore } from "../store/useFilterStore"; + +const frameworks = [ + { + value: "all", + label: "All", + }, + { + value: "pending", + label: "Pending", + }, + { + value: "approved", + label: "Approved", + }, + { + value: "rejected", + label: "Rejected", + }, +]; + +export function Status() { + const [open, setOpen] = React.useState(false); + const { statusFilter, setStatusFilter } = useFilterStore(); + + return ( + + + + + + + {/* */} + + No framework found. + + {frameworks.map((framework) => ( + { + setStatusFilter( + currentValue === statusFilter + ? "all" + : (currentValue as StatusFilterType), + ); + setOpen(false); + }} + > + + {framework.label} + + ))} + + + + + + ); +} diff --git a/frontend/src/components/TopFeeds.tsx b/frontend/src/components/TopFeeds.tsx new file mode 100644 index 00000000..1cc28c02 --- /dev/null +++ b/frontend/src/components/TopFeeds.tsx @@ -0,0 +1,113 @@ +import React, { useState, useEffect } from "react"; +import { + TwitterSubmissionWithFeedData, + SubmissionStatus, +} from "../types/twitter"; +import { Badge } from "./ui/badge"; +import { HiExternalLink } from "react-icons/hi"; +import { getTweetUrl } from "../lib/twitter"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "../components/ui/card"; + +// Updated formatDate function to show hours if posted within 24 hours +const formatDate = (dateString: string) => { + const tweetDate = new Date(dateString); + const now = new Date(); + const diffMs = now.getTime() - tweetDate.getTime(); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + + // If posted within the last 24 hours, show "Xh" format + if (diffHours < 24) { + return `${diffHours}h`; + } + + // Otherwise, show the date + return tweetDate.toLocaleDateString(); +}; + +interface TopFeedsProps { + items: TwitterSubmissionWithFeedData[]; + statusFilter: "all" | SubmissionStatus; + title?: string; +} + +const TopFeeds: React.FC = ({ + items, + statusFilter, + title = "Top Feeds", +}) => { + const [filteredItems, setFilteredItems] = useState< + TwitterSubmissionWithFeedData[] + >([]); + + useEffect(() => { + // Filter items based on status + const filtered = items.filter((item) => { + if (statusFilter === "all") return true; + return item.status === statusFilter; + }); + + // Sort by submission date (newest first) + const sorted = filtered.sort((a, b) => { + const dateB = b.submittedAt ? new Date(b.submittedAt).getTime() : 0; + const dateA = a.submittedAt ? new Date(a.submittedAt).getTime() : 0; + return dateB - dateA; + }); + + // Take only the top 3 + setFilteredItems(sorted.slice(0, 3)); + }, [items, statusFilter]); + + if (filteredItems.length === 0) { + return null; + } + + return ( + + + {title} + + + {filteredItems.map((item) => ( +
+
+
+
+

@{item.username}

+
+
+ + {formatDate(item.createdAt)} +
+
+ {item.status} +
+ +

+ {item.title || "Untitled"} + + + +

+ +

{item.content}

+
+ ))} +
+
+ ); +}; + +export default TopFeeds; diff --git a/frontend/src/components/UserDropdown.tsx b/frontend/src/components/UserDropdown.tsx new file mode 100644 index 00000000..02cd6f9f --- /dev/null +++ b/frontend/src/components/UserDropdown.tsx @@ -0,0 +1,57 @@ +"use client"; + +import * as React from "react"; +import { DropdownMenuCheckboxItemProps } from "@radix-ui/react-dropdown-menu"; + +import { Button } from "./ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; +import { AvatarDemo } from "./Avatar"; + +type Checked = DropdownMenuCheckboxItemProps["checked"]; + +export function DropdownMenuCheckboxes() { + const [showStatusBar, setShowStatusBar] = React.useState(true); + const [showActivityBar, setShowActivityBar] = React.useState(false); + const [showPanel, setShowPanel] = React.useState(false); + + return ( + + + + + + Appearance + + + Status Bar + + + Activity Bar + + + Panel + + + + ); +} diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx new file mode 100644 index 00000000..ca58dcf2 --- /dev/null +++ b/frontend/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "src/lib/utils"; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 00000000..63cbd44d --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "../../lib/utils"; + +const badgeVariants = cva( + "flex items-center justify-center rounded-md text-center border border-neutral-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 dark:border-neutral-800 dark:focus:ring-neutral-300", + { + variants: { + variant: { + default: + "border-transparent bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/80 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/80", + secondary: + "border-transparent bg-neutral-100 text-neutral-900 hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80", + destructive: + "border-transparent bg-red-500 text-neutral-50 shadow hover:bg-red-500/80 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/80", + outline: "text-neutral-950 dark:text-neutral-50", + approved: + "py-[4px] rounded-md border-none text-sm bg-green-100 min-w-[80px] text-green-600 leading-6 capitalize font-medium", + rejected: + "py-[4px] rounded-md border-none text-sm bg-red-100 min-w-[80px] text-red-600 leading-6 capitalize font-medium", + pending: + "py-[4px] rounded-md border-none text-center text-sm bg-yellow-100 min-w-[80px] text-yellow-600 leading-6 capitalize font-medium", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 00000000..18af10af --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "src/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-neutral-300", + { + variants: { + variant: { + default: + "bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90", + destructive: + "bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90", + outline: + "border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50", + secondary: + "bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80", + ghost: + "hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50", + link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 00000000..e330f45f --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,83 @@ +import * as React from "react"; + +import { cn } from "src/lib/utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/frontend/src/components/ui/command.tsx b/frontend/src/components/ui/command.tsx new file mode 100644 index 00000000..7b8176d9 --- /dev/null +++ b/frontend/src/components/ui/command.tsx @@ -0,0 +1,153 @@ +"use client"; + +import * as React from "react"; +import { type DialogProps } from "@radix-ui/react-dialog"; +import { Command as CommandPrimitive } from "cmdk"; +import { Search } from "lucide-react"; + +import { cn } from "src/lib/utils"; +import { Dialog, DialogContent } from "src/components/ui/dialog"; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 00000000..fa9c44a1 --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "src/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/frontend/src/components/ui/dropdown-menu.tsx b/frontend/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..3ea9d33d --- /dev/null +++ b/frontend/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,202 @@ +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "src/lib/utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0 dark:focus:bg-neutral-800 dark:focus:text-neutral-50", + inset && "pl-8", + className, + )} + {...props} + /> +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/frontend/src/components/ui/popover.tsx b/frontend/src/components/ui/popover.tsx new file mode 100644 index 00000000..c9bb0efd --- /dev/null +++ b/frontend/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; + +import { cn } from "src/lib/utils"; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverAnchor = PopoverPrimitive.Anchor; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/frontend/src/index.css b/frontend/src/index.css index 8f6e4afa..3976580e 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,22 +1,139 @@ -@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"); @tailwind base; @tailwind components; @tailwind utilities; +@import "tailwindcss"; + +@plugin 'tailwindcss-animate'; + +@custom-variant dark (&:is(.dark *)); @font-face { - font-family: 'Londrina Solid'; - src: url('../public/fonts/LondrinaSolid-NNS.ttf') format('truetype'); + font-family: "Londrina Solid"; + src: url("../public/fonts/LondrinaSolid-NNS.ttf") format("truetype"); font-weight: normal; font-style: normal; } +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + @layer base { body { @apply bg-white font-sans; } - h1, h2, h3 { + h1, + h2, + h3 { @apply font-londrina; } @@ -32,7 +149,8 @@ @apply text-xl font-black; } - p, .text-base { + p, + .text-base { @apply font-sans; } } @@ -83,7 +201,7 @@ word-break: break-word; hyphens: auto; } - + .custom-scrollbar { scrollbar-width: thin; scrollbar-color: black transparent; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 00000000..eb1caef3 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(...inputs)); +} diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index efa2d3fa..d843152e 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -8,6 +8,8 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. +import { createFileRoute } from "@tanstack/react-router"; + // Import Routes import { Route as rootRoute } from "./routes/__root"; @@ -16,10 +18,22 @@ import { Route as SettingsImport } from "./routes/settings"; import { Route as LeaderboardImport } from "./routes/leaderboard"; import { Route as IndexImport } from "./routes/index"; import { Route as FeedIndexImport } from "./routes/feed/index"; +import { Route as ExploreIndexImport } from "./routes/explore/index"; import { Route as FeedFeedIdImport } from "./routes/feed/$feedId"; +import { Route as ExploreRootImport } from "./routes/explore/_root"; + +// Create Virtual Routes + +const ExploreImport = createFileRoute("/explore")(); // Create/Update Routes +const ExploreRoute = ExploreImport.update({ + id: "/explore", + path: "/explore", + getParentRoute: () => rootRoute, +} as any); + const TestRoute = TestImport.update({ id: "/test", path: "/test", @@ -50,12 +64,23 @@ const FeedIndexRoute = FeedIndexImport.update({ getParentRoute: () => rootRoute, } as any); +const ExploreIndexRoute = ExploreIndexImport.update({ + id: "/", + path: "/", + getParentRoute: () => ExploreRoute, +} as any); + const FeedFeedIdRoute = FeedFeedIdImport.update({ id: "/feed/$feedId", path: "/feed/$feedId", getParentRoute: () => rootRoute, } as any); +const ExploreRootRoute = ExploreRootImport.update({ + id: "/_root", + getParentRoute: () => ExploreRoute, +} as any); + // Populate the FileRoutesByPath interface declare module "@tanstack/react-router" { @@ -88,6 +113,20 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof TestImport; parentRoute: typeof rootRoute; }; + "/explore": { + id: "/explore"; + path: "/explore"; + fullPath: "/explore"; + preLoaderRoute: typeof ExploreImport; + parentRoute: typeof rootRoute; + }; + "/explore/_root": { + id: "/explore/_root"; + path: "/explore"; + fullPath: "/explore"; + preLoaderRoute: typeof ExploreRootImport; + parentRoute: typeof ExploreRoute; + }; "/feed/$feedId": { id: "/feed/$feedId"; path: "/feed/$feedId"; @@ -95,6 +134,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof FeedFeedIdImport; parentRoute: typeof rootRoute; }; + "/explore/": { + id: "/explore/"; + path: "/"; + fullPath: "/explore/"; + preLoaderRoute: typeof ExploreIndexImport; + parentRoute: typeof ExploreImport; + }; "/feed/": { id: "/feed/"; path: "/feed"; @@ -107,12 +153,27 @@ declare module "@tanstack/react-router" { // Create and export the route tree +interface ExploreRouteChildren { + ExploreRootRoute: typeof ExploreRootRoute; + ExploreIndexRoute: typeof ExploreIndexRoute; +} + +const ExploreRouteChildren: ExploreRouteChildren = { + ExploreRootRoute: ExploreRootRoute, + ExploreIndexRoute: ExploreIndexRoute, +}; + +const ExploreRouteWithChildren = + ExploreRoute._addFileChildren(ExploreRouteChildren); + export interface FileRoutesByFullPath { "/": typeof IndexRoute; "/leaderboard": typeof LeaderboardRoute; "/settings": typeof SettingsRoute; "/test": typeof TestRoute; + "/explore": typeof ExploreRootRoute; "/feed/$feedId": typeof FeedFeedIdRoute; + "/explore/": typeof ExploreIndexRoute; "/feed": typeof FeedIndexRoute; } @@ -121,6 +182,7 @@ export interface FileRoutesByTo { "/leaderboard": typeof LeaderboardRoute; "/settings": typeof SettingsRoute; "/test": typeof TestRoute; + "/explore": typeof ExploreIndexRoute; "/feed/$feedId": typeof FeedFeedIdRoute; "/feed": typeof FeedIndexRoute; } @@ -131,7 +193,10 @@ export interface FileRoutesById { "/leaderboard": typeof LeaderboardRoute; "/settings": typeof SettingsRoute; "/test": typeof TestRoute; + "/explore": typeof ExploreRouteWithChildren; + "/explore/_root": typeof ExploreRootRoute; "/feed/$feedId": typeof FeedFeedIdRoute; + "/explore/": typeof ExploreIndexRoute; "/feed/": typeof FeedIndexRoute; } @@ -142,17 +207,29 @@ export interface FileRouteTypes { | "/leaderboard" | "/settings" | "/test" + | "/explore" | "/feed/$feedId" + | "/explore/" | "/feed"; fileRoutesByTo: FileRoutesByTo; - to: "/" | "/leaderboard" | "/settings" | "/test" | "/feed/$feedId" | "/feed"; + to: + | "/" + | "/leaderboard" + | "/settings" + | "/test" + | "/explore" + | "/feed/$feedId" + | "/feed"; id: | "__root__" | "/" | "/leaderboard" | "/settings" | "/test" + | "/explore" + | "/explore/_root" | "/feed/$feedId" + | "/explore/" | "/feed/"; fileRoutesById: FileRoutesById; } @@ -162,6 +239,7 @@ export interface RootRouteChildren { LeaderboardRoute: typeof LeaderboardRoute; SettingsRoute: typeof SettingsRoute; TestRoute: typeof TestRoute; + ExploreRoute: typeof ExploreRouteWithChildren; FeedFeedIdRoute: typeof FeedFeedIdRoute; FeedIndexRoute: typeof FeedIndexRoute; } @@ -171,6 +249,7 @@ const rootRouteChildren: RootRouteChildren = { LeaderboardRoute: LeaderboardRoute, SettingsRoute: SettingsRoute, TestRoute: TestRoute, + ExploreRoute: ExploreRouteWithChildren, FeedFeedIdRoute: FeedFeedIdRoute, FeedIndexRoute: FeedIndexRoute, }; @@ -189,6 +268,7 @@ export const routeTree = rootRoute "/leaderboard", "/settings", "/test", + "/explore", "/feed/$feedId", "/feed/" ] @@ -205,9 +285,24 @@ export const routeTree = rootRoute "/test": { "filePath": "test.tsx" }, + "/explore": { + "filePath": "explore", + "children": [ + "/explore/_root", + "/explore/" + ] + }, + "/explore/_root": { + "filePath": "explore/_root.tsx", + "parent": "/explore" + }, "/feed/$feedId": { "filePath": "feed/$feedId.tsx" }, + "/explore/": { + "filePath": "explore/index.tsx", + "parent": "/explore" + }, "/feed/": { "filePath": "feed/index.tsx" } diff --git a/frontend/src/routes/explore/_root.tsx b/frontend/src/routes/explore/_root.tsx new file mode 100644 index 00000000..abf0ecae --- /dev/null +++ b/frontend/src/routes/explore/_root.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/explore/_root")({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/explore/_root"!
; +} diff --git a/frontend/src/routes/explore/index.tsx b/frontend/src/routes/explore/index.tsx new file mode 100644 index 00000000..52d66ef5 --- /dev/null +++ b/frontend/src/routes/explore/index.tsx @@ -0,0 +1,162 @@ +import { createFileRoute } from "@tanstack/react-router"; +import Header from "../../components/Header"; +import { Button } from "../../components/ui/button"; +import SubmissionList from "../../components/SubmissionList"; +import InfiniteFeed from "../../components/InfiniteFeed"; +import { useBotId } from "../../lib/config"; +import { useEffect } from "react"; +import { useAllSubmissions } from "../../lib/api"; +import { Status } from "../../components/StatusFilter"; +import { Sort } from "../../components/Sort"; +import { useFilterStore } from "../../store/useFilterStore"; +import { + SubmissionStatus, + TwitterSubmissionWithFeedData, +} from "../../types/twitter"; + +export const Route = createFileRoute("/explore/")({ + component: ExplorePage, +}); + +type FeedSectionProps = { + title: string; + items: unknown[]; + fetchNextPage: () => void; + hasNextPage: boolean | undefined; + isFetchingNextPage: boolean; + status: string; + statusFilter: string; + botId: string; + showAll?: boolean; + showSort?: boolean; + actionButton?: { + label: string; + onClick?: () => void; + }; +}; + +const FeedSection = ({ + title, + items, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + status, + statusFilter, + botId, + showAll = true, + showSort = false, + actionButton, +}: FeedSectionProps) => ( +
+
+

{title}

+
+ + {showSort && } + {actionButton && ( + + )} +
+
+ ( + + )} + /> +
+); + +function ExplorePage() { + const botId = useBotId(); + const { statusFilter } = useFilterStore(); + + // Fetch submissions with infinite scroll + const ITEMS_PER_PAGE = 20; + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + status, + refetch, + } = useAllSubmissions( + ITEMS_PER_PAGE, + statusFilter === "all" ? undefined : statusFilter, + ); + + useEffect(() => { + refetch(); + }, [statusFilter, refetch]); + + const items = data?.items || []; + + return ( +
+
+
+ {/* Hero section */} +
+
+

+ Explore +

+

+ Discover autonomous brands powered by curators and AI. +
Find feeds that match your interests and contribute to + their growth. +

+
+
+ + +
+
+ + {/* Recent Submissions Section */} + + + {/* Feeds Section */} + +
+
+ ); +} diff --git a/frontend/src/store/useFilterStore.tsx b/frontend/src/store/useFilterStore.tsx new file mode 100644 index 00000000..8c466fac --- /dev/null +++ b/frontend/src/store/useFilterStore.tsx @@ -0,0 +1,23 @@ +import { create } from "zustand"; +import { TwitterSubmission } from "../types/twitter"; + +export type StatusFilterType = "all" | TwitterSubmission["status"]; +export type SortOrderType = "newest" | "oldest"; + +interface FilterState { + // Filter values + statusFilter: StatusFilterType; + sortOrder: SortOrderType; + + // Actions + setStatusFilter: (status: StatusFilterType) => void; +} + +export const useFilterStore = create((set) => ({ + // Initial state + statusFilter: "all", + sortOrder: "newest", + + // Actions + setStatusFilter: (status) => set({ statusFilter: status }), +})); diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 7b22e96f..558dfa5c 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,4 +1,5 @@ /** @type {import('tailwindcss').Config} */ +import typography from "@tailwindcss/typography"; export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { @@ -14,5 +15,5 @@ export default { }, }, }, - plugins: [require("@tailwindcss/typography")], + plugins: [typography], }; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1ffef600..b3a69870 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -3,5 +3,11 @@ "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } - ] + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + } } diff --git a/litefs.yml b/litefs.yml index 7b43006a..19a15246 100644 --- a/litefs.yml +++ b/litefs.yml @@ -6,7 +6,7 @@ fuse: dir: "/litefs" allow-other: true -# The data section describes settings for the internal LiteFS storage. We'll +# The data section describes settings for the internal LiteFS storage. We'll # mount a volume to the data directory so it can be persisted across restarts. # However, this data should not be accessed directly by the user application. data: @@ -24,7 +24,7 @@ proxy: addr: ":8080" target: "localhost:3000" db: "db" - passthrough: + passthrough: - "*.ico" - "*.png" diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index e4e07117..957157c1 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -1,22 +1,27 @@ # Active Context ## Current Focus + Platform Stability and Feature Enhancement ### Background + - Successfully operating with Node.js/Hono backend - Maintaining Bun for package management and development - Plugin system fully operational with multiple distributors and transformers - Multiple active feeds with Twitter-based curation ### Next Phase + 1. **Platform Stability** + - Monitoring system performance - Ensuring reliable content processing - Maintaining plugin compatibility - Optimizing resource usage 2. **Feature Enhancement** + - Expanding distributor plugins - Improving transformation capabilities - Enhancing curator experience @@ -29,6 +34,7 @@ Platform Stability and Feature Enhancement - Documenting configuration options ### Key Considerations + - Ensuring reliable content processing - Supporting growing number of feeds - Maintaining plugin compatibility @@ -38,6 +44,7 @@ Platform Stability and Feature Enhancement ## Active Decisions ### Architecture + 1. Node.js/Hono in production - Stable and reliable - Good performance characteristics @@ -53,30 +60,35 @@ Platform Stability and Feature Enhancement - Proper MIME type handling ### Plugin System + - Runtime module federation for plugins - Type-safe plugin configuration - Hot-reloading support - Standardized interfaces for different plugin types ### Content Flow + - Twitter as primary content source - Trusted curator moderation - Configurable transformation pipeline - Multi-channel distribution ## Current Focus Areas + 1. System reliability and performance 2. Plugin ecosystem expansion 3. Curator experience improvement 4. Documentation maintenance ## Next Steps + 1. Enhance recap functionality 2. Expand distributor options 3. Improve transformation capabilities 4. Optimize resource usage ## Validated Solutions + 1. Twitter-based submission and moderation 2. Plugin-based architecture 3. Configuration-driven feed management diff --git a/memory-bank/productContext.md b/memory-bank/productContext.md index 32dc9998..2392f1c0 100644 --- a/memory-bank/productContext.md +++ b/memory-bank/productContext.md @@ -1,14 +1,18 @@ # Product Context ## Problem Statement + In the fast-paced world of blockchain and technology, staying updated with quality information is challenging. Information is scattered across platforms, mixed with noise, and often lacks proper curation. This creates: + 1. Information overload for readers 2. Difficulty in finding reliable, domain-specific updates 3. Fragmented distribution of important updates 4. Inconsistent formatting and presentation ## Solution + curate.fun solves these problems by: + 1. Leveraging Twitter as a universal content submission platform 2. Using domain experts as content curators 3. Distributing curated content through multiple channels @@ -18,6 +22,7 @@ curate.fun solves these problems by: ## User Experience Goals ### For Content Consumers + - Access high-quality, curated content in their preferred platform - Get updates organized by specific domains (grants, ethereum, near, etc.) - Receive content in a consistent, well-formatted manner @@ -25,6 +30,7 @@ curate.fun solves these problems by: - Access content through multiple distribution channels ### For Curators + - Easy content moderation through Twitter - Clear visibility of submission status - Ability to add curator notes @@ -32,6 +38,7 @@ curate.fun solves these problems by: - Simple approval workflow using Twitter interactions ### For Feed Managers + - Flexible configuration of feed parameters - Control over curator lists - Customizable distribution channels @@ -41,37 +48,44 @@ curate.fun solves these problems by: ## Key Features ### Content Submission + - Twitter-based submission system - Support for multiple content types - Automatic content processing - Submission tracking and status updates ### Content Moderation + - Trusted curator network - Per-feed moderation settings - Status tracking system - Twitter-based approval workflow ### Content Transformation + - AI-powered content enhancement - Custom formatting per feed - Object mapping and transformation - Curator note integration ### Content Distribution + - Multi-platform distribution (Telegram, RSS, Notion, NEAR Social) - Custom formatting per distribution channel - Support for both stream and recap outputs - Configurable distribution rules ### Platform Management + - Configuration-driven feed management - Plugin-based architecture - Extensible transformation system - Comprehensive admin interface ## Active Feeds + The platform currently supports multiple active feeds including: + - Crypto Grant Wire (blockchain grant updates) - This Week in Ethereum (Ethereum ecosystem updates) - NEARWEEK (NEAR Protocol updates) @@ -83,6 +97,7 @@ The platform currently supports multiple active feeds including: - And many more specialized feeds ## Success Metrics + 1. Number of active feeds 2. Curator engagement levels 3. Content throughput diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 6f5d44d5..0cf41932 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -3,6 +3,7 @@ ## Current Status ### Working + - Frontend application with React and TanStack Router - Backend with Node.js/Hono - Plugin system with multiple distributors and transformers @@ -15,6 +16,7 @@ ### Platform Features #### Core System ✓ + - [x] Content submission via Twitter - [x] Trusted curator moderation - [x] Content processing pipeline @@ -24,6 +26,7 @@ - [x] Database storage and retrieval #### Distribution ✓ + - [x] Telegram channel distribution - [x] RSS feed generation - [x] Notion database integration @@ -31,6 +34,7 @@ - [x] Custom formatting per feed #### Transformation ✓ + - [x] Simple text transformation - [x] Object mapping transformation - [x] AI-powered content enhancement @@ -38,6 +42,7 @@ - [x] JSON sanitization throughout pipeline #### Frontend ✓ + - [x] Feed management interface - [x] Submission viewing and filtering - [x] Moderation information display @@ -45,22 +50,26 @@ - [x] Responsive design ### In Progress + - [ ] Recap functionality - [ ] Enhanced analytics - [ ] Additional distributor plugins - [ ] Performance optimization ## Next Actions + 1. Complete recap functionality 2. Implement performance monitoring 3. Expand distributor options 4. Enhance transformation capabilities ## Known Issues + - None critical - System is stable and operational - ~~JSON parsing errors in transformation pipeline~~ - Fixed with sanitization ## Feed Status + - Active feeds: Multiple (Ethereum, NEAR, Solana, Grants, AI, etc.) - Curator networks: Established for all active feeds - Distribution channels: Operational for all active feeds diff --git a/memory-bank/projectbrief.md b/memory-bank/projectbrief.md index f2df1640..f629b902 100644 --- a/memory-bank/projectbrief.md +++ b/memory-bank/projectbrief.md @@ -1,17 +1,20 @@ # Project Brief: curate.fun ## Overview + curate.fun is a content curation platform that aggregates and distributes curated content across various blockchain and technology domains. The platform uses Twitter as its primary content source and leverages a network of trusted curators for content moderation. It transforms and distributes content to multiple channels including Telegram, RSS feeds, Notion databases, and NEAR Social. ## Core Requirements ### Content Aggregation + - Monitor Twitter for content submissions - Support multiple content feeds (grants, ethereum, near, solana, ai, etc.) - Enable trusted curator moderation system - Track submission status and history ### Content Distribution + - Plugin-based distribution system supporting: - Telegram channels - RSS feeds @@ -21,6 +24,7 @@ curate.fun is a content curation platform that aggregates and distributes curate - Support for both stream and recap distribution modes ### Content Transformation + - Support content transformation before distribution - Enable both simple and AI-powered transformations - Support custom formatting per feed @@ -28,18 +32,21 @@ curate.fun is a content curation platform that aggregates and distributes curate - Curator note integration ### Moderation + - Twitter-based approver system - Per-feed moderator lists - Submission status tracking - Approval workflow via Twitter interactions ### Platform Management + - Configuration-driven feed management - Admin interface for monitoring and management - Plugin system for extensibility - Performance monitoring and analytics ## Goals + 1. Create a reliable content curation infrastructure 2. Enable easy content distribution across multiple platforms 3. Maintain high content quality through trusted curator network @@ -48,6 +55,7 @@ curate.fun is a content curation platform that aggregates and distributes curate 6. Enhance content with AI-powered transformations ## Technical Requirements + - High reliability and uptime - Scalable architecture - Plugin extensibility @@ -58,7 +66,9 @@ curate.fun is a content curation platform that aggregates and distributes curate - Graceful degradation ## Current Status + The platform is fully operational with: + - Multiple active feeds across various domains - Established curator networks - Multiple distribution channels diff --git a/memory-bank/systemPatterns.md b/memory-bank/systemPatterns.md index c1330589..4290c893 100644 --- a/memory-bank/systemPatterns.md +++ b/memory-bank/systemPatterns.md @@ -5,6 +5,7 @@ ### Core Components 1. **Server Layer (Hono)** + - REST API endpoints - Static file serving - CORS and security middleware @@ -12,6 +13,7 @@ - Process endpoint for content processing 2. **Service Layer** + - ConfigService: Configuration management - SubmissionService: Platform-agnostic submission handling - ProcessorService: Orchestrates transformation pipeline @@ -23,32 +25,34 @@ 3. **Plugin System** - Source plugins - * Twitter (primary content source) - * Telegram (message monitoring - planned) - * LinkedIn (planned integration) + - Twitter (primary content source) + - Telegram (message monitoring - planned) + - LinkedIn (planned integration) - Distributor plugins - * Telegram (@curatedotfun/telegram) - * RSS (@curatedotfun/rss) - * Notion (@curatedotfun/notion) - * NEAR Social (@curatedotfun/near-social) + - Telegram (@curatedotfun/telegram) + - RSS (@curatedotfun/rss) + - Notion (@curatedotfun/notion) + - NEAR Social (@curatedotfun/near-social) - Transformer plugins - * AI Transform (AI-powered content enhancement) - * Simple Transform (Basic formatting) - * Object Transform (Data mapping and transformation) + - AI Transform (AI-powered content enhancement) + - Simple Transform (Basic formatting) + - Object Transform (Data mapping and transformation) - Plugin Features - * Runtime loading and hot-reloading - * Type-safe configuration - * Custom endpoint registration - * Scheduled task integration - * Development toolkit with mocks + - Runtime loading and hot-reloading + - Type-safe configuration + - Custom endpoint registration + - Scheduled task integration + - Development toolkit with mocks ### Design Patterns 1. **Singleton Pattern** + - Used in ConfigService and PluginService for global configuration - Ensures consistent state across the application 2. **Plugin Pattern** + - Module federation for runtime plugin loading - Standardized plugin interfaces - Type-safe plugin configuration @@ -56,6 +60,7 @@ - Plugin caching and invalidation 3. **Service Pattern** + - Clear service boundaries and responsibilities - Platform-agnostic design - Encapsulated business logic @@ -63,6 +68,7 @@ - Extensible action handling 4. **Observer Pattern** + - Generic content source monitoring - Event-driven content processing - Configurable action handlers @@ -76,6 +82,7 @@ ## Component Relationships ### Configuration Flow + ```mermaid graph TD Config[ConfigService] --> Twitter[TwitterService] @@ -85,6 +92,7 @@ graph TD ``` ### Content Flow + ```mermaid graph LR Twitter[TwitterService] --> Submission[SubmissionService] @@ -97,6 +105,7 @@ graph LR ``` ### Plugin System + ```mermaid graph TD PluginService[PluginService] --> Load[Load Plugins] @@ -115,6 +124,7 @@ graph TD ``` ### Error Handling Flow + ```mermaid graph TD Error[Error Occurs] --> Type{Error Type} @@ -131,6 +141,7 @@ graph TD ## Key Technical Decisions 1. **Hono Framework** + - High performance - Built-in TypeScript support - Middleware ecosystem @@ -138,6 +149,7 @@ graph TD - Dynamic endpoint registration 2. **Plugin Architecture** + - Module federation for runtime loading - Type-safe plugin interfaces - Easy plugin development @@ -145,6 +157,7 @@ graph TD - Hot-reloading capability 3. **Configuration-Driven** + - JSON-based configuration - Runtime configuration updates - Environment variable support @@ -152,6 +165,7 @@ graph TD - Easy forking and customization 4. **Service Architecture** + - Platform-agnostic services - Clear service boundaries - Optimized transformer-distributor flow @@ -159,6 +173,7 @@ graph TD - Mock system for plugin validation 5. **Error Handling** + - Granular error types - Graceful degradation - Error recovery strategies @@ -166,6 +181,7 @@ graph TD - Error aggregation for multiple failures 6. **Task Scheduling** + - Configuration-driven cron jobs - Recap generation scheduling - Plugin-specific scheduled tasks diff --git a/memory-bank/techContext.md b/memory-bank/techContext.md index 3a934f99..afc73494 100644 --- a/memory-bank/techContext.md +++ b/memory-bank/techContext.md @@ -3,6 +3,7 @@ ## Technology Stack ### Backend + - **Runtime**: Node.js (production), Bun (development) - **Framework**: Hono - **Language**: TypeScript @@ -10,6 +11,7 @@ - **Build Tool**: RSPack ### Frontend + - **Framework**: React 18 - **Router**: TanStack Router - **State Management**: TanStack Query @@ -17,6 +19,7 @@ - **Styling**: Tailwind CSS ### External Services + - **Twitter API**: Content source and moderation - **Telegram API**: Content distribution - **Notion API**: Content distribution @@ -26,6 +29,7 @@ ## Development Setup ### Core Dependencies + - Node.js (runtime in production) - Bun (package manager and development runtime) - TypeScript (5.x+) @@ -35,32 +39,34 @@ - RSBuild & RSPack - Tailwind CSS - Testing Libraries - * Jest - * Testing Library - * Playwright + - Jest + - Testing Library + - Playwright ### Environment Configuration + - Core Settings - * NODE_ENV - * PORT - * LOG_LEVEL + - NODE_ENV + - PORT + - LOG_LEVEL - Twitter Auth - * TWITTER_USERNAME - * TWITTER_PASSWORD - * TWITTER_EMAIL - * TWITTER_2FA_SECRET + - TWITTER_USERNAME + - TWITTER_PASSWORD + - TWITTER_EMAIL + - TWITTER_2FA_SECRET - Distribution Settings - * TELEGRAM_BOT_TOKEN - * NOTION_API_KEY - * OPENROUTER_API_KEY - * SHIPPOST_NEAR_SOCIAL_KEY + - TELEGRAM_BOT_TOKEN + - NOTION_API_KEY + - OPENROUTER_API_KEY + - SHIPPOST_NEAR_SOCIAL_KEY - Plugin Settings - * PLUGIN_CACHE_TTL - * MAX_PLUGIN_MEMORY + - PLUGIN_CACHE_TTL + - MAX_PLUGIN_MEMORY ## Plugin System ### Core Plugin Features + - Runtime module federation loading - Hot-reloading support - Custom endpoint registration @@ -68,41 +74,46 @@ - Type-safe configuration ### Distributor Plugins + - Telegram: Real-time message distribution - RSS: Feed generation - Notion: Database integration - NEAR Social: Content posting ### Transformer Plugins + - AI Transform: AI-powered content transformation - Simple Transform: Basic content formatting - Object Transform: Data mapping and transformation ### Source Plugins + - Twitter: Tweet monitoring and interaction - Telegram: Message monitoring (planned) - LinkedIn: Post monitoring (planned) ### Plugin Development + - Development Tools - * Plugin development kit - * Type generation utilities - * Testing helpers - * Documentation generators + - Plugin development kit + - Type generation utilities + - Testing helpers + - Documentation generators - Testing Infrastructure - * Mock system - * Test runners - * Fixture generators - * Performance testing tools + - Mock system + - Test runners + - Fixture generators + - Performance testing tools - Development Features - * Hot-reload support - * Debug logging - * State inspection - * Performance profiling + - Hot-reload support + - Debug logging + - State inspection + - Performance profiling ## Task Scheduling ### Cron Jobs + - Configuration-driven scheduling - Recap generation tasks - Plugin-specific scheduled tasks @@ -110,6 +121,7 @@ - Error handling and retries ### Recap System + - Scheduled content aggregation - Customizable transformation - Multi-channel distribution @@ -118,12 +130,14 @@ ## Security Considerations ### API Security + - CORS with allowed origins configuration - Secure headers middleware - Cross-Origin policies - Content Security Policy ### Authentication & Authorization + - Twitter-based curator authentication - Environment-based service authentication - API endpoint access control @@ -131,18 +145,21 @@ ## Deployment ### Requirements + - Node.js environment - Environment variables configuration - Plugin dependencies - Frontend build artifacts ### Infrastructure + - Fly.io deployment - LiteFS for SQLite replication - Health check endpoint - Graceful shutdown handling ### Monitoring + - Health check endpoint - Service initialization status - Graceful shutdown handling @@ -151,45 +168,48 @@ ## Development Practices ### Code Organization + - Architecture - * Service-based design - * Plugin system - * Event-driven patterns - * Clean architecture principles + - Service-based design + - Plugin system + - Event-driven patterns + - Clean architecture principles - Standards - * TypeScript strict mode - * ESLint configuration - * Prettier formatting - * Import organization + - TypeScript strict mode + - ESLint configuration + - Prettier formatting + - Import organization - Component Design - * Atomic design principles - * Reusable patterns - * Performance optimization - * Error boundaries + - Atomic design principles + - Reusable patterns + - Performance optimization + - Error boundaries ### Testing Strategy + - Unit Testing - * Service tests - * Component tests - * Plugin tests - * Utility tests + - Service tests + - Component tests + - Plugin tests + - Utility tests - Integration Testing - * API endpoints - * Plugin interactions - * Service integration - * Event handling + - API endpoints + - Plugin interactions + - Service integration + - Event handling - E2E Testing - * User flows - * Plugin workflows - * Distribution paths - * Error scenarios + - User flows + - Plugin workflows + - Distribution paths + - Error scenarios - Performance Testing - * Load testing - * Stress testing - * Memory profiling - * Bottleneck identification + - Load testing + - Stress testing + - Memory profiling + - Bottleneck identification ### Project Structure + - Monorepo with Turborepo - Backend and Frontend - Shared types and utilities diff --git a/package.json b/package.json index 43d5a662..f516e772 100644 --- a/package.json +++ b/package.json @@ -20,5 +20,6 @@ "fmt": "prettier --write '**/*.{js,jsx,ts,tsx,json}'", "fmt:check": "prettier --check '**/*.{js,jsx,ts,tsx,json}'" }, - "type": "module" + "type": "module", + "packageManager": "bun@1.2.5" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 4c81417f..00000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,286 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - concurrently: - specifier: ^9.1.2 - version: 9.1.2 - prettier: - specifier: ^3.3.3 - version: 3.5.3 - turbo: - specifier: latest - version: 2.4.4 - -packages: - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - concurrently@9.1.2: - resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==} - engines: {node: '>=18'} - hasBin: true - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} - engines: {node: '>=14'} - hasBin: true - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - - shell-quote@1.8.2: - resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} - engines: {node: '>= 0.4'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - turbo-darwin-64@2.4.4: - resolution: {integrity: sha512-5kPvRkLAfmWI0MH96D+/THnDMGXlFNmjeqNRj5grLKiry+M9pKj3pRuScddAXPdlxjO5Ptz06UNaOQrrYGTx1g==} - cpu: [x64] - os: [darwin] - - turbo-darwin-arm64@2.4.4: - resolution: {integrity: sha512-/gtHPqbGQXDFhrmy+Q/MFW2HUTUlThJ97WLLSe4bxkDrKHecDYhAjbZ4rN3MM93RV9STQb3Tqy4pZBtsd4DfCw==} - cpu: [arm64] - os: [darwin] - - turbo-linux-64@2.4.4: - resolution: {integrity: sha512-SR0gri4k0bda56hw5u9VgDXLKb1Q+jrw4lM7WAhnNdXvVoep4d6LmnzgMHQQR12Wxl3KyWPbkz9d1whL6NTm2Q==} - cpu: [x64] - os: [linux] - - turbo-linux-arm64@2.4.4: - resolution: {integrity: sha512-COXXwzRd3vslQIfJhXUklgEqlwq35uFUZ7hnN+AUyXx7hUOLIiD5NblL+ETrHnhY4TzWszrbwUMfe2BYWtaPQg==} - cpu: [arm64] - os: [linux] - - turbo-windows-64@2.4.4: - resolution: {integrity: sha512-PV9rYNouGz4Ff3fd6sIfQy5L7HT9a4fcZoEv8PKRavU9O75G7PoDtm8scpHU10QnK0QQNLbE9qNxOAeRvF0fJg==} - cpu: [x64] - os: [win32] - - turbo-windows-arm64@2.4.4: - resolution: {integrity: sha512-403sqp9t5sx6YGEC32IfZTVWkRAixOQomGYB8kEc6ZD+//LirSxzeCHCnM8EmSXw7l57U1G+Fb0kxgTcKPU/Lg==} - cpu: [arm64] - os: [win32] - - turbo@2.4.4: - resolution: {integrity: sha512-N9FDOVaY3yz0YCOhYIgOGYad7+m2ptvinXygw27WPLQvcZDl3+0Sa77KGVlLSiuPDChOUEnTKE9VJwLSi9BPGQ==} - hasBin: true - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - -snapshots: - - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - concurrently@9.1.2: - dependencies: - chalk: 4.1.2 - lodash: 4.17.21 - rxjs: 7.8.2 - shell-quote: 1.8.2 - supports-color: 8.1.1 - tree-kill: 1.2.2 - yargs: 17.7.2 - - emoji-regex@8.0.0: {} - - escalade@3.2.0: {} - - get-caller-file@2.0.5: {} - - has-flag@4.0.0: {} - - is-fullwidth-code-point@3.0.0: {} - - lodash@4.17.21: {} - - prettier@3.5.3: {} - - require-directory@2.1.1: {} - - rxjs@7.8.2: - dependencies: - tslib: 2.8.1 - - shell-quote@1.8.2: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - tree-kill@1.2.2: {} - - tslib@2.8.1: {} - - turbo-darwin-64@2.4.4: - optional: true - - turbo-darwin-arm64@2.4.4: - optional: true - - turbo-linux-64@2.4.4: - optional: true - - turbo-linux-arm64@2.4.4: - optional: true - - turbo-windows-64@2.4.4: - optional: true - - turbo-windows-arm64@2.4.4: - optional: true - - turbo@2.4.4: - optionalDependencies: - turbo-darwin-64: 2.4.4 - turbo-darwin-arm64: 2.4.4 - turbo-linux-64: 2.4.4 - turbo-linux-arm64: 2.4.4 - turbo-windows-64: 2.4.4 - turbo-windows-arm64: 2.4.4 - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - y18n@5.0.8: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 From 30cbbfe99ece719c8f55ae4826a2efd3e0163143 Mon Sep 17 00:00:00 2001 From: Muhammad Saad Iqbal Date: Thu, 27 Mar 2025 08:46:12 +0500 Subject: [PATCH 04/90] Header + Explore Page Style (#113) * Explore Page - commit-1 * Explore Page - commit-2 * explore page - commit-3 * explore page - commit - 4 * explore page - commit - prettier * explore page responsiveness + code Rabbit Comments * code Rabbit Comments resolved * css updates * css changes 2 * header update + mobile responsive * conflicts resolved * Rebase and changes * Fix fmt * Header Updates + Web3Auth getUserInfo + Explore Page changes * fmt * coderabbit comments resolved --- frontend/src/App.tsx | 1 + frontend/src/components/Header.tsx | 221 ++++++++++------------ frontend/src/components/StatusFilter.tsx | 13 +- frontend/src/components/UserMenu.tsx | 108 +++++++++++ frontend/src/contexts/web3auth.tsx | 9 + frontend/src/routes/explore/index.tsx | 84 +++++--- frontend/src/store/useFeedFilterStore.tsx | 23 +++ frontend/src/types/web3auth.ts | 28 +++ 8 files changed, 338 insertions(+), 149 deletions(-) create mode 100644 frontend/src/components/UserMenu.tsx create mode 100644 frontend/src/store/useFeedFilterStore.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8b9bca0e..b35075ae 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { createRouter, RouterProvider } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; import { Web3AuthProvider } from "./contexts/web3auth"; +// import Header from "./components/Header"; // Set up a Router instance const router = createRouter({ diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index bc3221f0..4ed8729f 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,6 +1,5 @@ -import { FaTwitter, FaBook, FaGithub, FaTelegram } from "react-icons/fa"; import { Link } from "@tanstack/react-router"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Modal } from "./Modal"; import { HowItWorks } from "./HowItWorks"; import { useWeb3Auth } from "../hooks/use-web3-auth"; @@ -9,20 +8,48 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, DropdownMenuSeparator, - DropdownMenuShortcut, DropdownMenuTrigger, } from "./ui/dropdown-menu"; -import { AvatarDemo } from "./Avatar"; -import { ChevronDown, Menu, X } from "lucide-react"; + +import { + ChevronDown, + CircleUserRound, + CreditCard, + LogOut, + Menu, + X, +} from "lucide-react"; +import { AuthUserInfo } from "../types/web3auth"; +import UserMenu from "./UserMenu"; const Header = () => { const [showHowItWorks, setShowHowItWorks] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); - const { isInitialized, isLoggedIn, login, logout } = useWeb3Auth(); + const [userInfo, setUserInfo] = useState>(); + + const { isInitialized, isLoggedIn, login, logout, getUserInfo } = + useWeb3Auth(); + + useEffect(() => { + const fetchUserInfo = async () => { + try { + const info = await getUserInfo(); + setUserInfo(info); + console.log("User Info:", info); + } catch (error) { + console.error("Error fetching user info:", error); + } + }; + + if (isLoggedIn) { + fetchUserInfo(); + } else { + setUserInfo({}); + } + }, [isLoggedIn, getUserInfo]); return ( <> @@ -47,43 +74,17 @@ const Header = () => { - + +
-
- +
{" "}
- {/* User Dropdown */} - - - - - - My Account - - - - Log out - ⇧⌘Q - - - + {/* Mobile menu button */}
- - - - - - My Account - - - - Log out - ⇧⌘Q - - - + {isInitialized && isLoggedIn && userInfo ? ( + + + + + + +
+ Profile Image +
+

+ {(userInfo as { name?: string }).name} +

+

+ {(userInfo as { email?: string }).email} +

+
+
+
+ + + + Profile + + + + Wallet + + + + Disconnect + +
+
+ ) : ( + + )}
)} - -
- {isInitialized ? ( - isLoggedIn ? ( - - ) : ( - - ) - ) : ( -

Loading...

- )} -
- - setShowHowItWorks(false)}> diff --git a/frontend/src/components/StatusFilter.tsx b/frontend/src/components/StatusFilter.tsx index a82838e9..0951d4c3 100644 --- a/frontend/src/components/StatusFilter.tsx +++ b/frontend/src/components/StatusFilter.tsx @@ -13,7 +13,8 @@ import { CommandList, } from "./ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; -import { StatusFilterType, useFilterStore } from "../store/useFilterStore"; + +import { StatusFilterType } from "../store/useFilterStore"; const frameworks = [ { @@ -34,9 +35,13 @@ const frameworks = [ }, ]; -export function Status() { +interface StatusProps { + statusFilter: string; + setStatusFilter: (status: StatusFilterType) => void; +} + +export function Status({ statusFilter, setStatusFilter }: StatusProps) { const [open, setOpen] = React.useState(false); - const { statusFilter, setStatusFilter } = useFilterStore(); return ( @@ -68,7 +73,7 @@ export function Status() { setStatusFilter( currentValue === statusFilter ? "all" - : (currentValue as StatusFilterType), + : ((currentValue || "all") as StatusFilterType), ); setOpen(false); }} diff --git a/frontend/src/components/UserMenu.tsx b/frontend/src/components/UserMenu.tsx new file mode 100644 index 00000000..615e84e5 --- /dev/null +++ b/frontend/src/components/UserMenu.tsx @@ -0,0 +1,108 @@ +import { useEffect, useState } from "react"; + +import { useWeb3Auth } from "../hooks/use-web3-auth"; +import { Button } from "./ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; + +import { ChevronDown, CircleUserRound, CreditCard, LogOut } from "lucide-react"; +import { AuthUserInfo } from "../types/web3auth"; +export default function UserMenu() { + const [dropdownOpen, setDropdownOpen] = useState(false); + const [userInfo, setUserInfo] = useState>(); + + const { isInitialized, isLoggedIn, login, logout, getUserInfo } = + useWeb3Auth(); + + useEffect(() => { + const fetchUserInfo = async () => { + try { + const info = await getUserInfo(); + setUserInfo(info); + console.log("User Info:", info); + } catch (error) { + console.error("Error fetching user info:", error); + } + }; + + if (isLoggedIn) { + fetchUserInfo(); + } else { + setUserInfo({}); + } + }, [isLoggedIn, getUserInfo]); + return ( + <> + {isInitialized && isLoggedIn && userInfo ? ( + + + + + + +
+ Profile Image +
+

+ {(userInfo as { name?: string }).name} +

+

+ {(userInfo as { email?: string }).email} +

+
+
+
+ + + + Profile + + + + Wallet + + + + Disconnect + +
+
+ ) : ( + + )} + + ); +} diff --git a/frontend/src/contexts/web3auth.tsx b/frontend/src/contexts/web3auth.tsx index 9ccb2d5b..f0f4f727 100644 --- a/frontend/src/contexts/web3auth.tsx +++ b/frontend/src/contexts/web3auth.tsx @@ -108,6 +108,14 @@ export const Web3AuthProvider = ({ children }: Web3AuthProviderProps) => { } }; + const getUserInfo = async () => { + if (!web3auth || !provider) { + throw new Error("Web3Auth not initialized or provider not set"); + } + const userInfo = await web3auth.getUserInfo(); + return userInfo; + }; + return ( { isLoggedIn, login, logout, + getUserInfo, }} > {children} diff --git a/frontend/src/routes/explore/index.tsx b/frontend/src/routes/explore/index.tsx index 52d66ef5..1073b76d 100644 --- a/frontend/src/routes/explore/index.tsx +++ b/frontend/src/routes/explore/index.tsx @@ -8,12 +8,15 @@ import { useEffect } from "react"; import { useAllSubmissions } from "../../lib/api"; import { Status } from "../../components/StatusFilter"; import { Sort } from "../../components/Sort"; -import { useFilterStore } from "../../store/useFilterStore"; + +import { StatusFilterType, useFilterStore } from "../../store/useFilterStore"; import { SubmissionStatus, TwitterSubmissionWithFeedData, } from "../../types/twitter"; +import { useFeedFilterStore } from "../../store/useFeedFilterStore"; + export const Route = createFileRoute("/explore/")({ component: ExplorePage, }); @@ -33,6 +36,8 @@ type FeedSectionProps = { label: string; onClick?: () => void; }; + + setStatusFilter: (status: StatusFilterType) => void; }; const FeedSection = ({ @@ -44,15 +49,19 @@ const FeedSection = ({ status, statusFilter, botId, + + setStatusFilter, + showAll = true, showSort = false, actionButton, }: FeedSectionProps) => ( -
+

{title}

- + + {showSort && } {actionButton && ( + + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + + + + + + + +
+ ); +} diff --git a/frontend/src/components/profile/activity/index.tsx b/frontend/src/components/profile/activity/index.tsx new file mode 100644 index 00000000..9345a429 --- /dev/null +++ b/frontend/src/components/profile/activity/index.tsx @@ -0,0 +1,28 @@ +import { ChevronsUpDown } from "lucide-react"; +import { Button } from "../../ui/button"; +import { Card, CardContent } from "../../ui/card"; +import { PaginationControls } from "./PaginationControls"; +import { ActivityTable } from "./ActivityTable"; + +export function ProfileActivity() { + return ( +
+ + +
+

Activity Log

+ + +
+
+ +
+
+ + +
+ ); +} diff --git a/frontend/src/components/profile/content/SmallContainer.tsx b/frontend/src/components/profile/content/SmallContainer.tsx new file mode 100644 index 00000000..12678a31 --- /dev/null +++ b/frontend/src/components/profile/content/SmallContainer.tsx @@ -0,0 +1,202 @@ +import { cn } from "../../../lib/utils"; + +// Define status type for better type safety +type StatusType = "Approved" | "Pending" | "Rejected"; + +// Extract types to separate interface definitions +interface UserInfo { + profileImage: string; + username: string; + time: string; +} + +interface SmallContainerProps { + title: string; + description?: string; + status: StatusType; + className?: string; + moderationNotes?: { + moderator: string; + taggedUsers?: string[]; + }; + layout?: + | "default" + | "single-row" + | "profile-inline" + | "profile-stacked" + | "special"; + userInfo: UserInfo; +} + +// Extract status badge as a separate component +function StatusBadge({ status }: { status: StatusType }) { + const statusStyles = { + Approved: "text-green-600 bg-green-100", + Pending: "text-yellow-600 bg-yellow-100", + Rejected: "text-red-600 bg-red-100", + }; + + return ( +
+ {status} +
+ ); +} + +// Extract user profile component +function UserProfile({ profileImage, username, time }: UserInfo) { + return ( +
+ {username} + {username} + {time} +
+ ); +} + +// Extract moderation notes component +function ModerationNotes({ + moderator, + taggedUsers = [], +}: NonNullable) { + return ( +
+
+
+ Moderation Notes + by @{moderator} +
+ {taggedUsers.length > 0 && ( +
+ {taggedUsers.join(" ")} +
+ )} +
+
+ ); +} + +// Main component with layout-specific header rendering +function SmallContainer({ + title, + description, + status, + className, + moderationNotes, + layout = "default", + userInfo, +}: SmallContainerProps) { + // Render header based on layout type + const renderHeader = () => { + const { profileImage, username, time } = userInfo; + + switch (layout) { + case "single-row": + return ( +
+ + +
+ ); + + case "profile-inline": + return ( +
+
+ {username} + {username} + {time} + +
+
+ ); + + case "profile-stacked": + return ( + <> +
+ {username} + {username} +
+
+ {time} + +
+ + ); + + case "special": + return ( + <> +
+ {username} + {username} +
+
+ {time} + +
+ + ); + + case "default": + default: + return ( +
+
+ {username} + {username} + {time} +
+ +
+ ); + } + }; + + return ( +
+ {renderHeader()} + + {/* Title and description */} +

{title}

+ {description && ( +

{description}

+ )} + + {/* Moderation notes */} + {moderationNotes && } +
+ ); +} + +export default SmallContainer; diff --git a/frontend/src/components/profile/content/index.tsx b/frontend/src/components/profile/content/index.tsx new file mode 100644 index 00000000..7db18367 --- /dev/null +++ b/frontend/src/components/profile/content/index.tsx @@ -0,0 +1,239 @@ +import { Zap } from "lucide-react"; +import { Card, CardDescription, CardTitle } from "../../ui/card"; +import SmallContainer from "./SmallContainer"; + +interface ContainerData { + title: string; + description?: string; + status: "Approved" | "Pending" | "Rejected"; + moderationNotes?: { + moderator: string; + taggedUsers?: string[]; + }; + layout?: + | "default" + | "single-row" + | "profile-inline" + | "profile-stacked" + | "special"; + userInfo: { + profileImage: string; + username: string; + time: string; + }; +} + +export function ProfileContent() { + // Sample data with improved structure + const containers: ContainerData[] = [ + { + title: + "Arbitrum DAO's 7,500 ETH Allocation Faces Mixed Reactions Over Non-Native Projects", + description: + "The Arbitrum DAO plans to allocate 7,500 ETH through the Growth Management Committee to generate low-risk yield and support ecosystem growth, with specific allocations to Lido, Aave, and Fluid, but faces mixed community reactions due to concerns about the timing of the proposal and the choice of non-native projects. The proposal will be voted on February 27, 2025, and if rejected, the GMC will consider community feedback for alternative options.", + status: "Approved", + moderationNotes: { + moderator: "jpollock_", + taggedUsers: [ + "@karmaticacid", + "@ilhagirl", + "@yegorgolovnia", + "@PublicNouns", + ], + }, + layout: "default", + userInfo: { + profileImage: "/images/web3-plug.png", + username: "Web3Plug (morica/acc)", + time: "🇺🇸 · 2plugrel · 2h", + }, + }, + { + title: + "Arbitrum DAO's 7,500 ETH Allocation Faces Mixed Reactions Over Non-Native Project", + description: + "The Arbitrum DAO plans to allocate 7,500 ETH through the Growth Management Committee to generate low-risk yield and support ecosystem growth, with specific allocations to Lido, Aave, and Fluid, but faces mixed community reactions due to concerns about the timing of the proposal and the choice of non-native projects.", + status: "Approved", + moderationNotes: { + moderator: "jpollock_", + taggedUsers: [ + "@karmaticacid", + "@ilhagirl", + "@yegorgolovnia", + "@PublicNouns", + ], + }, + layout: "default", + userInfo: { + profileImage: "/images/web3-plug.png", + username: "Web3Plug (morica/acc)", + time: "🇺🇸 · 2plugrel · 2h", + }, + }, + { + title: + "Arbitrum DAO's 7,500 ETH Allocation Faces Mixed Reactions Over Non-Native Project", + description: + "The Arbitrum DAO plans to allocate 7,500 ETH through the Growth Management Committee to generate low-risk yield and support ecosystem growth.", + status: "Approved", + moderationNotes: { + moderator: "jpollock_", + taggedUsers: [ + "@karmaticacid", + "@ilhagirl", + "@yegorgolovnia", + "@PublicNouns", + ], + }, + layout: "default", + userInfo: { + profileImage: "/images/web3-plug.png", + username: "Web3Plug (morica/acc)", + time: "🇺🇸 · 2plugrel · 2h", + }, + }, + { + title: + "Arbitrum DAO's 7,500 ETH Allocation Faces Mixed Reactions Over Non-Native Projects", + description: + "The Arbitrum DAO plans to allocate 7,500 ETH through the Growth Management Committee to generate low-risk yield and support ecosystem growth, with specific allocations to Lido, Aave, and Fluid, but faces mixed community reactions due to concerns about the timing of the proposal and the choice of non-native projects. The proposal will be voted on February 27, 2025, and if rejected, the GMC will consider community feedback for alternative options.", + status: "Approved", + moderationNotes: { + moderator: "jpollock_", + taggedUsers: [ + "@karmaticacid", + "@ilhagirl", + "@yegorgolovnia", + "@PublicNouns", + ], + }, + layout: "default", + userInfo: { + profileImage: "/images/web3-plug.png", + username: "Web3Plug (morica/acc)", + time: "🇺🇸 · 2plugrel · 2h", + }, + }, + { + title: "Arbitrum DAO's 7,500 ETH Allocation", + status: "Approved", + moderationNotes: { + moderator: "jpollock_", + taggedUsers: [ + "@karmaticacid", + "@ilhagirl", + "@yegorgolovnia", + "@PublicNouns", + ], + }, + layout: "profile-stacked", + userInfo: { + profileImage: "/images/web3-plug.png", + username: "Web3Plug (morica/acc)", + time: "🇺🇸 · 2plugrel · 2h", + }, + }, + { + title: + "Arbitrum DAO's 7,500 ETH Allocation Faces Mixed Reactions Over Non-Native Projects", + description: + "The Arbitrum DAO plans to allocate 7,500 ETH through the Growth Management Committee to generate low-risk yield and support ecosystem growth, with specific allocations to Lido, Aave, and Fluid, but faces mixed community reactions due to concerns about the timing of the proposal and the choice of non-native projects.", + status: "Approved", + moderationNotes: { + moderator: "jpollock_", + taggedUsers: [ + "@karmaticacid", + "@ilhagirl", + "@yegorgolovnia", + "@PublicNouns", + ], + }, + layout: "profile-stacked", + userInfo: { + profileImage: "/images/web3-plug.png", + username: "Web3Plug (morica/acc)", + time: "🇺🇸 · 2plugrel · 2h", + }, + }, + ]; + + return ( +
+
+ +
+
+ +
+
+ + Top Performing + + + Your best content this week + +
+
+
+ +
+
+ +
+
+ + Top Performing + + + Your best content this week + +
+
+
+ +
+
+ +
+
+ + Top Performing + + + Your best content this week + +
+
+
+
+
+ {/* First row with two equal width sections */} +
+ {/* First container */} +
+ +
+ + {/* Container 1 and 2 stacked in a column */} +
+ + +
+
+ + {/* Second row with container 3 slightly larger */} +
+
+ +
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/frontend/src/components/profile/my-feeds/card.tsx b/frontend/src/components/profile/my-feeds/card.tsx new file mode 100644 index 00000000..5aa91e4c --- /dev/null +++ b/frontend/src/components/profile/my-feeds/card.tsx @@ -0,0 +1,81 @@ +import { Info, Newspaper, Users } from "lucide-react"; +import { Badge } from "../../ui/badge"; +import { cn } from "../../../lib/utils"; + +interface CardProps { + image: string; + title: string; + tags: string[]; + description: string; + createdAt: Date; + curators: number; + contents: number; + isCompleted: boolean; +} + +export function Card({ + image, + title, + tags, + description, + createdAt, + curators, + contents, + isCompleted, +}: CardProps) { + return ( +
+ {!isCompleted && ( +
+ + Setup incomplete +
+ )} +
+
+
+ Near Week +
+

+ {title} +

+
+ {tags.length > 0 && + tags.map((tag, index) => ( + + #{tag} + + ))} +
+
+
+

{description}

+

Created {createdAt.toLocaleDateString()}

+
+
+
+ +

+ {curators} Curators +

+
+
+ +

+ {contents} Contents +

+
+
+
+
+ ); +} diff --git a/frontend/src/components/profile/my-feeds/index.tsx b/frontend/src/components/profile/my-feeds/index.tsx new file mode 100644 index 00000000..5cef9b88 --- /dev/null +++ b/frontend/src/components/profile/my-feeds/index.tsx @@ -0,0 +1,129 @@ +import { Search } from "lucide-react"; +import { Input } from "../../ui/input"; +import { useState } from "react"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { cn } from "../../../lib/utils"; +import { Button } from "./../../ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "./../../ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "./../../ui/popover"; +import { Card } from "./card"; + +const feeds = [ + { + value: "all", + label: "All", + }, + { + value: "latest", + label: "Latest", + }, + { + value: "popular", + label: "Popular", + }, + { + value: "technology", + label: "Technology", + }, + { + value: "design", + label: "Design", + }, + { + value: "lifestyle", + label: "Lifestyle", + }, +]; + +const CardContent = { + title: "Near Week", + description: + "Near Week is a weekly newsletter that covers the latest developments in the blockchain space. It's a great way to stay up to date with the latest news and events in the crypto world.", + tags: ["near", "blockchain", "crypto"], + createdAt: new Date(), + curators: 5, + contents: 100, + image: "/images/near-week.png", + isCompleted: false, +}; + +export function MyFeeds() { + const [open, setOpen] = useState(false); + const [value, setValue] = useState("all"); + return ( +
+
+
+ + + +
+ + + + + + + + + No feed found. + + {feeds.map((feed) => ( + { + setValue(currentValue === value ? "" : currentValue); + setOpen(false); + }} + > + + {feed.label} + + ))} + + + + + + +
+
+
+ + + +
+
+ ); +} diff --git a/frontend/src/components/profile/overview/ApproverFor.tsx b/frontend/src/components/profile/overview/ApproverFor.tsx new file mode 100644 index 00000000..d8e9ce02 --- /dev/null +++ b/frontend/src/components/profile/overview/ApproverFor.tsx @@ -0,0 +1,70 @@ +import { Badge } from "../../ui/badge"; + +interface ItemProps { + image: string; + points: string; + name: string; + tags: string[]; +} + +function Item({ image, points, name, tags }: ItemProps) { + return ( +
+
+ +
+
{name}
+
+ {tags.length > 0 && + tags.map((tag, index) => ( + + #{tag} + + ))} +
+
+
+
+ {points} +
+
+ ); +} + +export function ApproverFor() { + return ( +
+
+

+ Approver For +

+

+ #No +

+
+
+ + + +
+
+ ); +} diff --git a/frontend/src/components/profile/overview/CurateCTA.tsx b/frontend/src/components/profile/overview/CurateCTA.tsx new file mode 100644 index 00000000..39e42597 --- /dev/null +++ b/frontend/src/components/profile/overview/CurateCTA.tsx @@ -0,0 +1,35 @@ +import { Button } from "../../ui/button"; + +export function CurateCTA() { + return ( +
+
+ +
+
No $CURATE? No Problem.
+

+ Getting CURATE is easier than ever with _______. Get your tokens in + minutes. +

+
+
+
+ + +
+
+ ); +} diff --git a/frontend/src/components/profile/overview/CuratorFor.tsx b/frontend/src/components/profile/overview/CuratorFor.tsx new file mode 100644 index 00000000..3d1a7760 --- /dev/null +++ b/frontend/src/components/profile/overview/CuratorFor.tsx @@ -0,0 +1,70 @@ +import { Badge } from "../../ui/badge"; + +interface ItemProps { + image: string; + points: string; + name: string; + tags: string[]; +} + +function Item({ image, points, name, tags }: ItemProps) { + return ( +
+
+ +
+
{name}
+
+ {tags.length > 0 && + tags.map((tag, index) => ( + + #{tag} + + ))} +
+
+
+
+ {points} +
+
+ ); +} + +export function CuratorFor() { + return ( +
+
+

+ Curator For +

+

+ #No +

+
+
+ + + +
+
+ ); +} diff --git a/frontend/src/components/profile/overview/TopBadges.tsx b/frontend/src/components/profile/overview/TopBadges.tsx new file mode 100644 index 00000000..e9beb224 --- /dev/null +++ b/frontend/src/components/profile/overview/TopBadges.tsx @@ -0,0 +1,59 @@ +import { BadgeCent } from "lucide-react"; + +interface BadgeProps { + image: string; + points: number; + name: string; + description: string; +} +function Badge({ image, points, name, description }: BadgeProps) { + return ( +
+
+ Badge +
+
{name}
+

{description}

+
+
+

+ {points} Points +

+
+ ); +} +export function TopBadges() { + return ( +
+

+ Top Badges +

+
+ + + + +
+
+ ); +} diff --git a/frontend/src/components/profile/overview/UserStats.tsx b/frontend/src/components/profile/overview/UserStats.tsx new file mode 100644 index 00000000..97e454b1 --- /dev/null +++ b/frontend/src/components/profile/overview/UserStats.tsx @@ -0,0 +1,44 @@ +interface StatCardProps { + title: string; + value: string; +} + +function StatCard({ title, value }: StatCardProps) { + return ( +
+

+ {value} +

+

+ {title} +

+
+ ); +} + +export function UserStats() { + return ( +
+
+

+ Welcome, +
+ 72d2......2532 +

+
+
+ Profile Background +
+
+
+ + + +
+
+ ); +} diff --git a/frontend/src/components/profile/overview/index.tsx b/frontend/src/components/profile/overview/index.tsx new file mode 100644 index 00000000..0a5345e5 --- /dev/null +++ b/frontend/src/components/profile/overview/index.tsx @@ -0,0 +1,21 @@ +import { ApproverFor } from "./ApproverFor"; +import { CurateCTA } from "./CurateCTA"; +import { CuratorFor } from "./CuratorFor"; +import { TopBadges } from "./TopBadges"; +import { UserStats } from "./UserStats"; + +export function ProfileOverview() { + return ( +
+
+ + +
+ +
+ + +
+
+ ); +} diff --git a/frontend/src/components/profile/points/BadgeCard.tsx b/frontend/src/components/profile/points/BadgeCard.tsx new file mode 100644 index 00000000..6be98263 --- /dev/null +++ b/frontend/src/components/profile/points/BadgeCard.tsx @@ -0,0 +1,64 @@ +import { Card, CardContent } from "../../../components/ui/card"; +import { BadgeCent } from "lucide-react"; + +interface BadgeItem { + title: string; + subtitle: string; + icon: React.ReactNode; + iconBgColor: string; + points: number; + achieved: boolean; + progress?: number; + total?: number; + iconName?: string; +} + +export function BadgeCard({ badge }: { badge: BadgeItem }) { + return ( + + +
+
+ {badge.icon} +
+

{badge.title}

+

{badge.subtitle}

+ +
+ + {!badge.progress && ( +
+ + {badge.points} Points + + {badge.achieved && ( + + )} +
+ )} + + {badge.progress !== undefined && badge.total !== undefined && ( +
+
+
+
+
+ 40 + {badge.points} Points +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/profile/points/PaginationControls.tsx b/frontend/src/components/profile/points/PaginationControls.tsx new file mode 100644 index 00000000..5ae759ae --- /dev/null +++ b/frontend/src/components/profile/points/PaginationControls.tsx @@ -0,0 +1,70 @@ +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationEllipsis, +} from "../../../components/ui/pagination"; + +export function PaginationControls() { + return ( +
+ + + + + + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + + + + + + + +
+ ); +} diff --git a/frontend/src/components/profile/points/PointsLogTable.tsx b/frontend/src/components/profile/points/PointsLogTable.tsx new file mode 100644 index 00000000..092b8709 --- /dev/null +++ b/frontend/src/components/profile/points/PointsLogTable.tsx @@ -0,0 +1,77 @@ +import { + Table, + TableBody, + TableCell, + TableRow, +} from "../../../components/ui/table"; + +import { ArrowDown, ArrowUp, ThumbsUp } from "lucide-react"; + +interface PointLogItem { + id: number; + type: string; + description: string; + points: number; + time: string; + isPositive: boolean; +} + +export function PointsLogTable({ data }: { data: PointLogItem[] }) { + return ( + + + {data.map((item) => ( + + +
+ {item.id === 4 ? ( + + ) : ( + + )} +
+
+ + +
+ Type +

+ {item.description} +

+
+
+ + +
+
+ +
+
+ + Points + +

+ {item.isPositive ? "+" : "-"} {item.points} +

+
+
+
+ + +
+ Time +

{item.time}

+
+
+
+ ))} +
+
+ ); +} diff --git a/frontend/src/components/profile/points/PointsOverviewCard.tsx b/frontend/src/components/profile/points/PointsOverviewCard.tsx new file mode 100644 index 00000000..2a63aab9 --- /dev/null +++ b/frontend/src/components/profile/points/PointsOverviewCard.tsx @@ -0,0 +1,60 @@ +import { Card, CardContent } from "../../../components/ui/card"; +import { Button } from "../../../components/ui/button"; +import { Coins, ChartNoAxesCombined } from "lucide-react"; + +export const PointsOverviewCard = ({ + totalPoints, + monthlyIncrease, + tokensEarned, +}: { + totalPoints: number; + monthlyIncrease: string; + tokensEarned: string; +}) => { + return ( +
+ + +
+
+ + Total Points + + + {totalPoints.toLocaleString()} + +

+ +{monthlyIncrease}{" "} + increase this month +

+
+ +
+
+
+ + + +
+
+ + Tokens Earned + + + {tokensEarned.toLocaleString()} + + +
+ +
+
+
+
+ ); +}; diff --git a/frontend/src/components/profile/points/index.tsx b/frontend/src/components/profile/points/index.tsx new file mode 100644 index 00000000..7da13cc2 --- /dev/null +++ b/frontend/src/components/profile/points/index.tsx @@ -0,0 +1,149 @@ +import { Award, ChevronsUpDown, FileText, Leaf, Search } from "lucide-react"; +import { PointsOverviewCard } from "./PointsOverviewCard"; +import { BadgeCard } from "./BadgeCard"; +import { Card, CardContent } from "../../ui/card"; +import { Button } from "../../ui/button"; +import { PointsLogTable } from "./PointsLogTable"; +import { PaginationControls } from "./PaginationControls"; + +const badgesData = [ + { + title: "Content Novice", + subtitle: "Successfully Create 50 Content", + icon: , + iconBgColor: "bg-yellow-200", + points: 100, + achieved: true, + iconName: "badge-cent", + }, + { + title: "Content Master", + subtitle: "Successfully Create 50 Content", + icon: , + iconBgColor: "bg-gray-300", + points: 100, + achieved: true, + iconName: "badge-cent", + }, + { + title: "Curator OG", + subtitle: "Successfully Curate 1000 Content", + icon: , + iconBgColor: "bg-blue-300", + points: 100, + achieved: true, + iconName: "badge-cent", + }, + { + title: "Content Master", + subtitle: "Successfully Create 50 Content", + icon: , + iconBgColor: "bg-green-200", + points: 500, + achieved: false, + progress: 40, + total: 100, + }, +]; + +const pointsLogData = [ + { + id: 1, + type: "Content Submission", + description: "Content Submission", + points: 50, + time: "10:24PM", + isPositive: true, + }, + { + id: 2, + type: "Approved Content", + description: "Approved Content", + points: 20, + time: "10:20PM", + isPositive: true, + }, + { + id: 3, + type: "Approved Content", + description: "Approved Content", + points: 20, + time: "10:20PM", + isPositive: true, + }, + { + id: 4, + type: "Points Redemption", + description: "Points Redemption", + points: 90, + time: "10:20PM", + isPositive: false, + }, + { + id: 5, + type: "Approved Content", + description: "Approved Content", + points: 20, + time: "10:20PM", + isPositive: true, + }, + { + id: 6, + type: "Approved Content", + description: "Approved Content", + points: 20, + time: "10:20PM", + isPositive: true, + }, + { + id: 7, + type: "Approved Content", + description: "Approved Content", + points: 20, + time: "10:20PM", + isPositive: true, + }, +]; + +export function ProfilePoints() { + return ( +
+
+

Points Overview

+ +
+ +
+

Badges & Achievement

+
+ {badgesData.map((badge, index) => ( + + ))} +
+
+ +
+ + +
+

Points Log

+ + +
+
+ +
+
+ + +
+
+ ); +} diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx index ca58dcf2..d9365988 100644 --- a/frontend/src/components/ui/avatar.tsx +++ b/frontend/src/components/ui/avatar.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import * as AvatarPrimitive from "@radix-ui/react-avatar"; -import { cn } from "src/lib/utils"; +import { cn } from "../../lib/utils"; const Avatar = React.forwardRef< React.ElementRef, diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx index 63cbd44d..114867e5 100644 --- a/frontend/src/components/ui/badge.tsx +++ b/frontend/src/components/ui/badge.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "../../lib/utils"; const badgeVariants = cva( - "flex items-center justify-center rounded-md text-center border border-neutral-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 dark:border-neutral-800 dark:focus:ring-neutral-300", + "flex items-center justify-center rounded-md text-center border border-neutral-200 p-1 text-xs font-normal transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 dark:border-neutral-800 dark:focus:ring-neutral-300", { variants: { variant: { diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 18af10af..9a9a4707 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "src/lib/utils"; +import { cn } from "../../lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-neutral-300", @@ -14,12 +14,16 @@ const buttonVariants = cva( destructive: "bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90", outline: - "border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50", + "border font-normal border-neutral-300 px-3 py-2.5 bg-white rounded hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50", secondary: "bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80", ghost: "hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50", link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50", + "outline-button": + "border border-black bg-white rounded-[4px] py-3 shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50", + filled: + "bg-black font-normal text-white rounded-[4px] px-4 py-3 hover:bg-black/80", }, size: { default: "h-9 px-4 py-2", diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index e330f45f..801d4fed 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { cn } from "src/lib/utils"; +import { cn } from "../../lib/utils"; const Card = React.forwardRef< HTMLDivElement, @@ -9,7 +9,7 @@ const Card = React.forwardRef<
>( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/frontend/src/components/ui/pagination.tsx b/frontend/src/components/ui/pagination.tsx new file mode 100644 index 00000000..6e062c35 --- /dev/null +++ b/frontend/src/components/ui/pagination.tsx @@ -0,0 +1,117 @@ +import * as React from "react"; +import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "../../lib/utils"; +import { ButtonProps, buttonVariants } from "./button"; + +const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( +