diff --git a/frontend/app/globals.css b/frontend/app/globals.css index a956fbd4..8657b283 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -99,3 +99,33 @@ img.emoji { width: 100%; margin: 0; } + +/* ── Design system animations ──────────────────────────────────── */ +@keyframes twinkle { + 0%, 100% { opacity: 0.2; } + 50% { opacity: 0.9; } +} + +@keyframes blink { + 0%, 50% { opacity: 1; } + 50.01%, 100% { opacity: 0; } +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes scaleIn { + from { opacity: 0; transform: scale(0.96) translateY(8px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +@keyframes rowHighlight { + 0% { background: rgba(248, 151, 254, 0.18); } + 100% { background: transparent; } +} + +.markee-row--new { + animation: rowHighlight 1.6s ease-out; +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 8a9f8d9d..24441732 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,34 +1,23 @@ 'use client' -import { useState, useCallback, useEffect } from 'react' +import { useState, useCallback, useEffect, useMemo } from 'react' import { useAccount } from 'wagmi' -import Link from 'next/link' import { Header } from '@/components/layout/Header' import { Footer } from '@/components/layout/Footer' import { useMarkees } from '@/lib/contracts/useMarkees' -import { useFixedMarkees } from '@/lib/contracts/useFixedMarkees' import { useReactions } from '@/hooks/useReactions' import { useViews } from '@/hooks/useViews' -import { useFixedViews } from '@/hooks/useFixedViews' -import { MarkeeCard } from '@/components/leaderboard/MarkeeCard' -import { LeaderboardSkeleton } from '@/components/leaderboard/MarkeeCardSkeleton' import { TopDawgModal } from '@/components/modals/TopDawgModal' -import { FixedPriceModal } from '@/components/modals/FixedPriceModal' -import { HeroBackground } from '@/components/backgrounds/HeroBackground' -import { Eye } from 'lucide-react' -import { formatEther } from 'viem' - -import { formatDistanceToNow } from 'date-fns' +import { Eye, Search } from 'lucide-react' +import { formatEth, formatAddress } from '@/lib/utils' import type { Markee } from '@/types' -import type { FixedMarkee } from '@/lib/contracts/useFixedMarkees' export default function Home() { const { address } = useAccount() const { markees, isLoading, isFetchingFresh, error, lastUpdated, refetch } = useMarkees() - const { markees: fixedMarkees, isLoading: isLoadingFixed } = useFixedMarkees() - // Ecosystem stats (same source as /ecosystem page) + // Ecosystem stats const [ecoLeaderboards, setEcoLeaderboards] = useState<{ topFundsAddedRaw: string; markeeCount: number; isLegacy?: boolean }[]>([]) const [ecoTotalFunds, setEcoTotalFunds] = useState('0') const [isLoadingEco, setIsLoadingEco] = useState(true) @@ -52,15 +41,8 @@ export default function Home() { 0 ) - const { - reactions, - toggleReaction, - removeReaction, - isLoading: reactionsLoading, - error: reactionsError, - } = useReactions() + const { reactions, toggleReaction, removeReaction, isLoading: reactionsLoading } = useReactions() - // ── Leaderboard view tracking ──────────────────────────────────────────────── const { views, trackView } = useViews(markees) useEffect(() => { @@ -69,27 +51,35 @@ export default function Home() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [markees.map(m => m.address).join(',')]) - // ── Hero readerboard view tracking ────────────────────────────────────────── - const { views: fixedViews, trackView: trackFixedView } = useFixedViews(fixedMarkees) - - useEffect(() => { - if (fixedMarkees.length === 0) return - fixedMarkees.forEach(trackFixedView) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fixedMarkees.map(m => m.strategyAddress).join(',')]) - // ──────────────────────────────────────────────────────────────────────────── + // Search state + const [search, setSearch] = useState('') + + // Rank lookup map + const rankMap = useMemo(() => { + const m = new Map() + markees.forEach((markee, i) => m.set(markee.address, i + 1)) + return m + }, [markees]) + + // Leaderboard rows = #2+ filtered by search + const leaderboardMarkees = useMemo(() => { + const base = markees.slice(1) + if (!search.trim()) return base + const s = search.toLowerCase() + return base.filter(m => + m.message.toLowerCase().includes(s) || + (m.name || '').toLowerCase().includes(s) || + m.address.toLowerCase().includes(s) || + m.owner.toLowerCase().includes(s) + ) + }, [markees, search]) const [isModalOpen, setIsModalOpen] = useState(false) const [selectedMarkee, setSelectedMarkee] = useState(null) const [modalMode, setModalMode] = useState<'create' | 'addFunds' | 'updateMessage'>('create') - const [isFixedModalOpen, setIsFixedModalOpen] = useState(false) - const [selectedFixedMarkee, setSelectedFixedMarkee] = useState(null) - const handleTransactionSuccess = useCallback(() => { - setTimeout(() => { - refetch() - }, 3000) + setTimeout(() => refetch(), 3000) }, [refetch]) const handleCreateNew = useCallback(() => { @@ -98,351 +88,318 @@ export default function Home() { setIsModalOpen(true) }, []) - const handleEditMessage = useCallback((markee: Markee) => { + const handleAddFunds = useCallback((markee: Markee) => { setSelectedMarkee(markee) - setModalMode('updateMessage') + setModalMode('addFunds') setIsModalOpen(true) }, []) - const handleAddFunds = useCallback((markee: Markee) => { + const handleEditMessage = useCallback((markee: Markee) => { setSelectedMarkee(markee) - setModalMode('addFunds') + setModalMode('updateMessage') setIsModalOpen(true) }, []) - const handleReact = useCallback( - async (markee: Markee, emoji: string) => { - if (!address) return - try { - await toggleReaction(markee.address, emoji, markee.chainId) - } catch (err) { - console.error('Failed to toggle reaction:', err) - } - }, - [address, toggleReaction] - ) + const handleReact = useCallback(async (markee: Markee, emoji: string) => { + if (!address) return + try { await toggleReaction(markee.address, emoji, markee.chainId) } + catch (err) { console.error('Failed to toggle reaction:', err) } + }, [address, toggleReaction]) - const handleRemoveReaction = useCallback( - async (markee: Markee) => { - if (!address) return - try { - await removeReaction(markee.address) - } catch (err) { - console.error('Failed to remove reaction:', err) - } - }, - [address, removeReaction] - ) + const handleRemoveReaction = useCallback(async (markee: Markee) => { + if (!address) return + try { await removeReaction(markee.address) } + catch (err) { console.error('Failed to remove reaction:', err) } + }, [address, removeReaction]) const handleModalClose = useCallback(() => { setIsModalOpen(false) setSelectedMarkee(null) }, []) - const handleFixedMarkeeClick = useCallback((fixedMarkee: FixedMarkee) => { - setSelectedFixedMarkee(fixedMarkee) - setIsFixedModalOpen(true) - }, []) - - const handleFixedModalClose = useCallback(() => { - setIsFixedModalOpen(false) - setSelectedFixedMarkee(null) - }, []) - - // Helper to get view counts for a leaderboard markee - const getViews = (markee: Markee) => { - const v = views.get(markee.address.toLowerCase()) - return { - totalViews: v?.totalViews, - messageViews: v?.messageViews, - } - } + const top = markees[0] return (
- {/* Hero */} -
- - -
-
- {isLoadingFixed ? ( - [1, 2, 3].map(i => ( -
-
-
-
-
- )) - ) : ( - fixedMarkees.map((fixedMarkee, index) => { - const viewData = fixedViews.get(fixedMarkee.strategyAddress.toLowerCase()) - return ( - - ) - }) - )} -
-
-
- - - - {/* Raise Funds with Markee */} -
-
-

Raise Funds with Markee

-

Join the growing network of digital communities getting funded with a Markee sign.

- -
-
- - {isLoadingEco ? ( - -- - ) : ( - {ecoActive.length} - )} - active Markees -
-
- - {isLoadingEco ? ( - -- - ) : ( - {ecoMessages.toLocaleString()} - )} - messages bought -
-
- - {isLoadingEco ? ( - -- - ) : ( - - {parseFloat(ecoTotalFunds) < 0.001 ? '< 0.001 ETH' : `${parseFloat(ecoTotalFunds).toFixed(3)} ETH`} - + {/* ── Hero — Featured #1 Markee ───────────────────────────────────── */} + {!isLoading && top && (() => { + const topViews = views.get(top.address.toLowerCase())?.totalViews + const hasName = top.name && top.name.trim() + + return ( +
+ {/* Scanlines */} +
+ + {/* Static star field */} +
+ + {/* FEATURED MESSAGE label */} +
+ + FEATURED MESSAGE + + {topViews !== undefined && ( + {topViews.toLocaleString()} views )} - total raised
-
- - - Create a Markee - -
-
- {/* Leaderboard */} -
-
-
-

A Marketplace for Digital Real Estate

+ {/* Big message in translucent bordered box */} +
+
+ + {top.message} + +
-

- Buy a message on your favorite site from these verified Markees. -

+ {/* Owner / meta row */} +
+ + {hasName ? top.name : formatAddress(top.owner)} + {hasName && ( + {formatAddress(top.owner)} + )} + + + {formatEth(top.totalFundsAdded)} ETH raised + +
+
-
+ {/* CTA row */} +
- How it Works + How it works
+
+ ) + })()} + + {/* ── Stats strip ─────────────────────────────────────────────────── */} +
+
+
+
+ {isLoadingEco + ? + : ecoActive.length} +
+
active Markees
- -
-
- {(isFetchingFresh || reactionsLoading) && ( -
-
- Updating... -
- )} - {lastUpdated && !isLoading && ( -
- Last updated {formatDistanceToNow(lastUpdated, { addSuffix: true })} -
- )} +
+
+ {isLoadingEco + ? + : ecoMessages.toLocaleString()} +
+
messages bought
+
+
+
+ {isLoadingEco + ? + : parseFloat(ecoTotalFunds) < 0.001 + ? '< 0.001' + : parseFloat(ecoTotalFunds).toFixed(3)} + {!isLoadingEco && ETH}
+
total funds raised
+ +
+
- {reactionsError && ( -
-

{reactionsError}

+ {/* ── Leaderboard ──────────────────────────────────────────────────── */} +
+ {/* Title row */} +
+
+

Top Markees

+

+ Everyone below can be bumped. Pay to jump the queue. +

+
+ {(isFetchingFresh || reactionsLoading) && ( +
+
+ Updating…
)} +
- {isLoading && } - - {!isLoading && markees.length > 0 && ( - <> - {/* #1 Hero */} - - - - - {/* #2-3 Large */} -
- {markees.slice(1, 3).map((markee, i) => ( - - - - ))} -
+ {/* Search bar */} +
+
+ + setSearch(e.target.value)} + placeholder="search messages, owners, 0x…" + className="flex-1 bg-transparent border-none text-[#EDEEFF] py-2.5 px-2.5 text-[13px] outline-none placeholder:text-[#8A8FBF]" + /> + {search && ( + + )} +
+
- {/* #4-26 Medium */} -
- {markees.slice(3, 26).map((markee, i) => ( - - - - ))} -
+ {/* Dense table */} + {isLoading ? ( +
+ ) : markees.length > 0 && ( +
+ {/* Column header */} +
+ RANK + MESSAGE + + VIEWS + RAISED + TOP PRICE +
- {/* #27+ List */} - {markees.length > 26 && ( -
- {markees.slice(26).map((markee, i) => ( - - - - ))} -
+ {/* Rows */} +
+ {leaderboardMarkees.map((markee, i) => { + const viewData = views.get(markee.address.toLowerCase()) + const hasName = markee.name && markee.name.trim() + const rank = rankMap.get(markee.address) ?? i + 2 + const viewCount = viewData?.totalViews + const fmtViews = viewCount === undefined ? '—' + : viewCount >= 1_000_000 ? `${(viewCount / 1_000_000).toFixed(1)}M` + : viewCount >= 1_000 ? `${(viewCount / 1_000).toFixed(1)}k` + : String(viewCount) + + return ( +
handleAddFunds(markee)} + className="cursor-pointer border-b border-[#8A8FBF]/20 last:border-0 transition-colors hover:bg-[#F897FE]/[0.04]" + style={{ display: 'grid', gridTemplateColumns: '40px 1fr 140px 80px 110px 110px', gap: 14, padding: '10px 14px', alignItems: 'center' }} + > + #{rank} +
+
{markee.message}
+
+ + — {hasName ? markee.name : formatAddress(markee.owner)} + + + + {fmtViews} + + + {formatEth(markee.totalFundsAdded)} ETH + + + {formatEth(markee.totalFundsAdded + 1000000000000000n)} ETH + +
+ ) + })} + {leaderboardMarkees.length === 0 && ( +
No results for “{search}”
)} - - )} -
+
+
+ )}
@@ -453,14 +410,7 @@ export default function Home() { userMarkee={selectedMarkee} initialMode={modalMode} onSuccess={handleTransactionSuccess} - topFundsAdded={markees[0]?.totalFundsAdded} - /> - -
) diff --git a/frontend/components/layout/Footer.tsx b/frontend/components/layout/Footer.tsx index 88abf2b4..0fb6a9a9 100644 --- a/frontend/components/layout/Footer.tsx +++ b/frontend/components/layout/Footer.tsx @@ -1,60 +1,56 @@ 'use client' +function SocialIcon({ href, label, children }: { href: string; label: string; children: React.ReactNode }) { + return ( + + {children} + + ) +} + export function Footer() { return ( -