Skip to content

Commit 52874e7

Browse files
authored
feat(home): add rich footer with nav, socials, and noggles copy (#102)
Replaces minimal NogglesCopyFooter on the home route with a fuller layout (logo + tagline, three nav columns mirroring the header, X/Snapshot social buttons, copyright, and the kept ⌐◨-◨ click-to-copy easter egg). Other routes still render the original minimal footer via a route-aware wrapper.
1 parent 5a279b2 commit 52874e7

3 files changed

Lines changed: 216 additions & 4 deletions

File tree

src/app/layout.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import { DaoHeader } from "@/components/layout/DaoHeader";
77
// import { MuralBackground } from "@/components/layout/MuralBackground";
88
import Providers from "@/components/layout/Providers";
99
import { ScrollToTop } from "@/components/layout/ScrollToTop";
10+
import { SiteFooter } from "@/components/layout/SiteFooter";
1011
import { MiniAppReady } from "@/components/miniapp/MiniAppReady";
11-
import { NogglesCopyFooter } from "@/components/home/NogglesCopyFooter";
12+
import { MiniTV } from "@/components/tv/MiniTV";
13+
import { MiniTVVisibilityProvider } from "@/components/tv/MiniTVVisibilityContext";
1214
import { Toaster } from "@/components/ui/sonner";
1315
import { TooltipProvider } from "@/components/ui/tooltip";
14-
import { MiniTVVisibilityProvider } from "@/components/tv/MiniTVVisibilityContext";
15-
import { MiniTV } from "@/components/tv/MiniTV";
1616
import { MINIAPP_EMBED_CONFIG } from "@/lib/miniapp-config";
1717
import "./globals.css";
1818

@@ -85,7 +85,7 @@ export default function RootLayout({
8585
<DaoHeader />
8686
<AAOnboarding />
8787
<main className="max-w-6xl mx-auto px-4">{children}</main>
88-
<NogglesCopyFooter />
88+
<SiteFooter />
8989
<Toaster />
9090
<MiniTV />
9191
</MiniTVVisibilityProvider>

src/components/home/HomeFooter.tsx

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import Image from "next/image";
5+
import Link from "next/link";
6+
import { Vote } from "lucide-react";
7+
import { toast } from "sonner";
8+
import { Badge } from "@/components/ui/badge";
9+
import { DAO_DESCRIPTION } from "@/lib/config";
10+
import { cn } from "@/lib/utils";
11+
12+
const NOGGLES_ASCII = "⌐◨-◨";
13+
14+
const NAV_COLUMNS: ReadonlyArray<{
15+
title: string;
16+
links: ReadonlyArray<{ label: string; href: string; external?: boolean }>;
17+
}> = [
18+
{
19+
title: "Governance",
20+
links: [
21+
{ label: "Proposals", href: "/proposals" },
22+
{ label: "Create Proposal", href: "/propose" },
23+
{ label: "Auctions", href: "/auctions" },
24+
{ label: "Treasury", href: "/treasury" },
25+
],
26+
},
27+
{
28+
title: "Community",
29+
links: [
30+
{ label: "Members", href: "/members" },
31+
{ label: "Blogs", href: "/blogs" },
32+
{ label: "Propdates", href: "/propdates" },
33+
{ label: "Bounties", href: "/community/bounties" },
34+
],
35+
},
36+
{
37+
title: "Explore",
38+
links: [
39+
{ label: "TV", href: "/tv" },
40+
{ label: "Droposals", href: "/droposals" },
41+
{ label: "NogglesRails", href: "/nogglesrails" },
42+
{ label: "Swap", href: "/swap" },
43+
],
44+
},
45+
];
46+
47+
function XIcon({ className }: { className?: string }) {
48+
return (
49+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
50+
<path d="M18.244 2H21.5l-7.51 8.58L23 22h-6.91l-5.41-6.69L4.4 22H1.144l8.04-9.19L1 2h7.09l4.88 6.13L18.244 2Zm-2.42 18.18h1.92L7.27 3.72H5.21l10.614 16.46Z" />
51+
</svg>
52+
);
53+
}
54+
55+
function fallbackCopyTextToClipboard(text: string): boolean {
56+
try {
57+
const textarea = document.createElement("textarea");
58+
textarea.value = text;
59+
textarea.setAttribute("readonly", "");
60+
textarea.style.position = "fixed";
61+
textarea.style.top = "-9999px";
62+
textarea.style.left = "-9999px";
63+
document.body.appendChild(textarea);
64+
textarea.select();
65+
const ok = document.execCommand("copy");
66+
document.body.removeChild(textarea);
67+
return ok;
68+
} catch {
69+
return false;
70+
}
71+
}
72+
73+
export function HomeFooter({ className }: { className?: string }) {
74+
const year = new Date().getFullYear();
75+
76+
const onCopyNoggles = React.useCallback(async () => {
77+
try {
78+
if (navigator.clipboard?.writeText) {
79+
await navigator.clipboard.writeText(NOGGLES_ASCII);
80+
} else {
81+
const ok = fallbackCopyTextToClipboard(NOGGLES_ASCII);
82+
if (!ok) throw new Error("copy failed");
83+
}
84+
toast.success("Copied");
85+
} catch {
86+
toast.error("Failed to copy");
87+
}
88+
}, []);
89+
90+
return (
91+
<footer
92+
className={cn(
93+
"mt-16 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60",
94+
className,
95+
)}
96+
>
97+
<div className="mx-auto max-w-6xl px-4 py-12 md:px-6">
98+
<div className="grid grid-cols-2 gap-8 md:grid-cols-4 lg:grid-cols-5">
99+
{/* Brand */}
100+
<div className="col-span-2 lg:col-span-2 flex flex-col gap-4">
101+
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
102+
<div className="flex aspect-square size-9 items-center justify-center overflow-hidden rounded-md bg-black">
103+
<Image
104+
src="/gnars.webp"
105+
alt="Gnars DAO"
106+
width={64}
107+
height={64}
108+
className="object-cover w-5 h-5"
109+
/>
110+
</div>
111+
<div className="flex flex-col gap-0.5 leading-none">
112+
<span className="font-semibold text-base">Gnars DAO</span>
113+
<Badge
114+
variant="secondary"
115+
className="h-4 w-fit px-1.5 text-[10px] bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-200"
116+
>
117+
Base
118+
</Badge>
119+
</div>
120+
</Link>
121+
<p className="text-sm text-muted-foreground max-w-sm leading-relaxed">
122+
{DAO_DESCRIPTION}.
123+
</p>
124+
<div className="flex items-center gap-2">
125+
<a
126+
href="https://x.com/gnars_dao"
127+
target="_blank"
128+
rel="noopener noreferrer"
129+
aria-label="Gnars on X"
130+
title="Gnars on X"
131+
className="flex size-9 items-center justify-center rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
132+
>
133+
<XIcon className="size-4" />
134+
</a>
135+
<a
136+
href="https://snapshot.box/#/s:gnars.eth"
137+
target="_blank"
138+
rel="noopener noreferrer"
139+
aria-label="Gnars Snapshot space"
140+
title="Gnars Snapshot space"
141+
className="flex size-9 items-center justify-center rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
142+
>
143+
<Vote className="size-4" />
144+
</a>
145+
</div>
146+
</div>
147+
148+
{/* Nav columns */}
149+
{NAV_COLUMNS.map((column) => (
150+
<div key={column.title} className="flex flex-col gap-3">
151+
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
152+
{column.title}
153+
</div>
154+
<ul className="flex flex-col gap-2">
155+
{column.links.map((link) => (
156+
<li key={link.href}>
157+
{link.external ? (
158+
<a
159+
href={link.href}
160+
target="_blank"
161+
rel="noopener noreferrer"
162+
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
163+
>
164+
{link.label}
165+
</a>
166+
) : (
167+
<Link
168+
href={link.href}
169+
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
170+
>
171+
{link.label}
172+
</Link>
173+
)}
174+
</li>
175+
))}
176+
</ul>
177+
</div>
178+
))}
179+
</div>
180+
181+
<div className="mt-10 flex flex-col-reverse items-center justify-between gap-4 border-t pt-6 sm:flex-row">
182+
<p className="text-xs text-muted-foreground">© {year} Gnars DAO · Built on Base</p>
183+
<p className="font-sans text-sm text-muted-foreground">
184+
made with{" "}
185+
<a
186+
href="#"
187+
onClick={(event) => {
188+
event.preventDefault();
189+
void onCopyNoggles();
190+
}}
191+
className="hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
192+
aria-label='Copy "⌐◨-◨"'
193+
title='Copy "⌐◨-◨"'
194+
>
195+
{NOGGLES_ASCII}
196+
</a>
197+
</p>
198+
</div>
199+
</div>
200+
</footer>
201+
);
202+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"use client";
2+
3+
import { usePathname } from "next/navigation";
4+
import { HomeFooter } from "@/components/home/HomeFooter";
5+
import { NogglesCopyFooter } from "@/components/home/NogglesCopyFooter";
6+
7+
export function SiteFooter() {
8+
const pathname = usePathname();
9+
return pathname === "/" ? <HomeFooter /> : <NogglesCopyFooter />;
10+
}

0 commit comments

Comments
 (0)