diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 9ac131df..14f254a6 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -6,7 +6,7 @@ import { MinimizedAudioBar } from "@/components/MinimizedAudioBar"; import { AudioPlayerProvider } from "@/contexts/AudioPlayerContext"; import { sidebarItemIcon } from "@/lib/sidebarItems"; import { Toaster } from "./components/ui/toaster"; -import { MainLayout } from "./components/MainLayout"; +import { FundraiserLayout } from "./components/FundraiserLayout"; import { ScrollToTop } from "./components/ScrollToTop"; import { VersionCheck } from "./components/VersionCheck"; import { useCurrentUser } from "./hooks/useCurrentUser"; @@ -24,11 +24,9 @@ const ReplyComposeModal = lazy(() => import("@/components/ReplyComposeModal").th // Lazy-loaded emoji pack dialog const EmojiPackDialog = lazy(() => import("@/components/EmojiPackDialog").then(m => ({ default: m.EmojiPackDialog }))); -// HomePage eagerly imported all page components; now lazy-loaded -const HomePage = lazy(() => import("./pages/HomePage").then(m => ({ default: m.HomePage }))); - -// Campaigns: lazy-loaded list + create pages. (Detail page is dispatched from -// NIP19Page when an naddr resolves to kind 30223.) +// Campaigns: home + create. (Campaign detail is dispatched from NIP19Page +// when an naddr resolves to kind 30223.) The campaigns list IS the homepage; +// the configurable HomePage delegation from the Twitter-era app is gone. const CampaignsPage = lazy(() => import("./pages/CampaignsPage").then(m => ({ default: m.CampaignsPage }))); const CreateCampaignPage = lazy(() => import("./pages/CreateCampaignPage").then(m => ({ default: m.CreateCampaignPage }))); @@ -159,11 +157,11 @@ export function AppRouter() { {/* Auto-follow deep link: fullscreen immersive (no sidebars/nav) */} } /> - {/* All routes share the persistent MainLayout (sidebar + nav) */} - }> - } /> + {/* All routes share the persistent FundraiserLayout (top nav + footer) */} + }> + } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/components/FundraiserLayout.tsx b/src/components/FundraiserLayout.tsx new file mode 100644 index 00000000..bf209e0d --- /dev/null +++ b/src/components/FundraiserLayout.tsx @@ -0,0 +1,105 @@ +import { Suspense, useCallback, useMemo, useRef, useState } from 'react'; +import { Outlet } from 'react-router-dom'; + +import { TopNav } from '@/components/TopNav'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + CenterColumnContext, + DrawerContext, + LayoutStore, + LayoutStoreContext, + NavHiddenContext, +} from '@/contexts/LayoutContext'; +import { cn } from '@/lib/utils'; + +/** + * Persistent app shell for the fundraising-platform overhaul. + * + * Replaces the previous Twitter-style three-column `MainLayout` with a + * GoFundMe-style top-nav-only chrome. Routes render in a single full-width + * content area below the {@link TopNav}. + * + * Compatibility surface: + * - We still provide `LayoutStoreContext`, so pages that call + * `useLayoutOptions(...)` keep working. Most options (FAB, sidebars, + * mobile arc) are intentionally ignored here because the new shell has + * no FAB and no sidebars. The store still drives the + * `wrapperClassName` escape hatch for pages that need to widen. + * - `CenterColumnContext` exposes the content `
` so legacy components + * (e.g. nsite preview overlay) can still portal into it. + * - `DrawerContext` and `NavHiddenContext` are kept as no-op providers so + * pages that read them don't crash. + */ + +function PageSkeleton() { + return ( +
+ + + + +
+ ); +} + +function FundraiserLayoutInner() { + const centerColumnRef = useRef(null); + const [centerColumnEl, setCenterColumnEl] = useState(null); + + // Mobile drawer is owned by TopNav now, so consumers of `useOpenDrawer` + // become no-ops. Keeping the context shape avoids touching every page that + // pulls the hook. + const openDrawer = useCallback(() => {}, []); + + return ( + + + +
+ + + }> +
{ + centerColumnRef.current = el; + setCenterColumnEl(el); + }} + className={cn('flex-1 min-w-0 w-full')} + > + +
+
+ + +
+
+
+
+ ); +} + +function SiteFooter() { + return ( +
+
+ © {new Date().getFullYear()} Agora. Fundraisers on Nostr. + +
+
+ ); +} + +export function FundraiserLayout() { + const store = useMemo(() => new LayoutStore(), []); + return ( + + + + ); +} + +export default FundraiserLayout; diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx new file mode 100644 index 00000000..00dd94b3 --- /dev/null +++ b/src/components/TopNav.tsx @@ -0,0 +1,196 @@ +import { useState } from 'react'; +import { Link, NavLink } from 'react-router-dom'; +import { Menu, PlusCircle, X } from 'lucide-react'; + +import { LoginArea } from '@/components/auth/LoginArea'; +import { Button } from '@/components/ui/button'; +import { LogoIcon } from '@/components/icons/LogoIcon'; +import { Sheet, SheetContent } from '@/components/ui/sheet'; +import { useAppContext } from '@/hooks/useAppContext'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { cn } from '@/lib/utils'; + +interface NavItem { + label: string; + to: string; + /** If true, this link is treated as active only on an exact match. */ + exact?: boolean; +} + +const NAV_ITEMS: NavItem[] = [ + { label: 'Discover', to: '/', exact: true }, + { label: 'Start a campaign', to: '/campaigns/new' }, + { label: 'About', to: '/help' }, +]; + +/** + * Persistent top navigation bar rendered by {@link FundraiserLayout}. Mirrors + * the GoFundMe-style chrome: brand mark on the left, primary nav links in the + * middle, "Sign in" / account avatar plus a "Start a campaign" pill on the + * right. Collapses to a hamburger menu below the `md` breakpoint. + */ +export function TopNav() { + const { config } = useAppContext(); + const { user } = useCurrentUser(); + const [mobileOpen, setMobileOpen] = useState(false); + + return ( +
+
+ {/* Mobile menu trigger */} + + + {/* Brand */} + + + {config.appName} + + + {/* Desktop nav */} + + +
+ + {/* Right cluster */} +
+ {/* Primary CTA pill — hidden on small screens to keep the bar uncluttered; + the same action lives at the top of the mobile menu and as a FAB-style + button in the homepage hero. */} + + + {/* LoginArea handles both logged-in (account avatar dropdown) and + logged-out (Log in / Sign up) states. We render it inline-flex + and let it style its own children. */} + +
+
+ + {/* Mobile drawer */} + + +
+ setMobileOpen(false)} + className="flex items-center gap-2 font-bold text-lg text-primary" + > + + {config.appName} + + +
+ +
+ + setMobileOpen(false)} /> +
+
+
+
+ ); +} + +function NavLinkButton({ item }: { item: NavItem }) { + return ( + + cn( + 'px-3 py-2 rounded-md text-sm font-medium motion-safe:transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', + isActive + ? 'text-foreground' + : 'text-muted-foreground hover:text-foreground hover:bg-secondary', + ) + } + > + {item.label} + + ); +} + +/** + * Secondary links inside the mobile drawer for the logged-in user — quick + * shortcuts to the parts of the app that live outside the fundraising flow + * but should still be reachable. + */ +function SecondaryMobileLinks({ onClose }: { onClose: () => void }) { + const { user } = useCurrentUser(); + if (!user) return null; + + const items: { label: string; to: string }[] = [ + { label: 'Wallet', to: '/wallet' }, + { label: 'Bitcoin', to: '/bitcoin' }, + { label: 'Notifications', to: '/notifications' }, + { label: 'Profile', to: '/profile' }, + { label: 'Settings', to: '/settings' }, + ]; + return ( +
    + {items.map((item) => ( +
  • + + {item.label} + +
  • + ))} +
+ ); +} diff --git a/src/pages/CampaignDetailPage.tsx b/src/pages/CampaignDetailPage.tsx index deba44a2..50eb86d5 100644 --- a/src/pages/CampaignDetailPage.tsx +++ b/src/pages/CampaignDetailPage.tsx @@ -119,25 +119,22 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { return (
- {/* Sticky-ish top bar with back button */} -
-
+
+ {/* Inline back arrow — the global TopNav handles primary navigation. */} +
-

{campaign.title}

-
-
-
{/* Hero */}
@@ -239,7 +236,7 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) {
{/* Donation rail */} -