diff --git a/package-lock.json b/package-lock.json index 05e8e20..f81d6f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "date-fns": "^4.1.0", "dexie": "^4.2.1", "dexie-react-hooks": "^4.2.0", + "embla-carousel-react": "^8.6.0", "flexsearch": "^0.8.212", "framer-motion": "^12.23.26", "hash-sum": "^2.0.0", @@ -6738,6 +6739,34 @@ "devOptional": true, "license": "ISC" }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", diff --git a/package.json b/package.json index 160f261..c5d7f0d 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "date-fns": "^4.1.0", "dexie": "^4.2.1", "dexie-react-hooks": "^4.2.0", + "embla-carousel-react": "^8.6.0", "flexsearch": "^0.8.212", "framer-motion": "^12.23.26", "hash-sum": "^2.0.0", diff --git a/src/components/GrimoireWelcome.tsx b/src/components/GrimoireWelcome.tsx index 6791e47..ac7574d 100644 --- a/src/components/GrimoireWelcome.tsx +++ b/src/components/GrimoireWelcome.tsx @@ -1,71 +1,26 @@ -import { Terminal } from "lucide-react"; +import { Terminal, Github, MessageSquare, ArrowRight } from "lucide-react"; import { Button } from "./ui/button"; import { Kbd, KbdGroup } from "./ui/kbd"; -import { Progress } from "./ui/progress"; -import { MONTHLY_GOAL_SATS } from "@/services/supporters"; -import { useLiveQuery } from "dexie-react-hooks"; -import db from "@/services/db"; +import { SpellShowcase } from "./landing/SpellShowcase"; +import { SpellbookShowcase } from "./landing/SpellbookShowcase"; +import { TestimonialsSection } from "./landing/TestimonialsSection"; +import { SupportSection } from "./landing/SupportSection"; interface GrimoireWelcomeProps { onLaunchCommand: () => void; onExecuteCommand: (command: string) => void; } -const EXAMPLE_COMMANDS = [ - { - command: "zap grimoire.rocks", - description: "Support Grimoire development", - showProgress: true, - }, - { - command: "chat groups.0xchat.com'NkeVhXuWHGKKJCpn", - description: "Join the Grimoire welcome chat", - }, - { - command: "profile fiatjaf.com", - description: "Explore a Nostr profile", - }, - { command: "req -k 1 -l 20", description: "Query recent notes" }, -]; - -export function GrimoireWelcome({ - onLaunchCommand, - onExecuteCommand, -}: GrimoireWelcomeProps) { - // Calculate monthly donations reactively from DB (last 30 days) - const monthlyDonations = - useLiveQuery(async () => { - const thirtyDaysAgo = Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60; - let total = 0; - await db.grimoireZaps - .where("timestamp") - .aboveOrEqual(thirtyDaysAgo) - .each((zap) => { - total += zap.amountSats; - }); - return total; - }, []) ?? 0; - - // Calculate progress - const goalProgress = (monthlyDonations / MONTHLY_GOAL_SATS) * 100; - - // Format sats - function formatSats(sats: number): string { - if (sats >= 1_000_000) { - return `${(sats / 1_000_000).toFixed(1)}M`; - } else if (sats >= 1_000) { - return `${Math.floor(sats / 1_000)}k`; - } - return sats.toString(); - } - +export function GrimoireWelcome({ onLaunchCommand }: GrimoireWelcomeProps) { return ( -
- {` ★ ✦
+
+
+ {/* Hero Section */}
+
+ {/* Desktop: ASCII art */}
+
+
+ {` ★ ✦
: ☽
t#, ,;
✦ .Gt j. t ;##W. t j. f#i
@@ -80,76 +35,113 @@ export function GrimoireWelcome({
✦ j###t E#t ;#W: E#t :K#t ##D. E#t G#t E#t E#t ;#W: .D#;
.G#t DWi ,KK: E#t ... #G .. t E#t DWi ,KK: tt
;; ☆ ,;. j ✦ ,;. ☆ `}
-
-
- a nostr client for magicians
-
+
+
+ a nostr client for magicians
+
+
+
+ {/* Mobile: Simple text */}
+
+
+ grimoire
+
+
+ a nostr client for magicians
+
+
+
+ {/* Tagline */}
+
+
+ A tiling window manager for the Nostr protocol
+
+
+ Explore feeds, profiles, and events with Unix-style commands.
+ Craft powerful queries with spells. Save entire workspaces with
+ spellbooks.
+
+
+
+ {/* CTA Button */}
+
+
+ Press
+
+ Cmd
+ +
+ K
+
+ or
+
+ Ctrl
+ +
+ K
+
+ to get started
+
+
+
- {/* Mobile: Simple text */}
-
-
- grimoire
-
-
- a nostr client for magicians
-
-
+ {/* Divider */}
+
- {/* Launch button */}
-
-
- Press
-
- Cmd
- +
- K
-
- or
-
- Ctrl
- +
- K
-
-
-
-
+ {/* Spell Showcase */}
+
- {/* Example commands */}
-
-
- Try these commands:
-
- {EXAMPLE_COMMANDS.map(({ command, description, showProgress }) => (
-
- ))}
-
+ {/* Divider */}
+
+
+ {/* Spellbook Showcase */}
+
+
+ {/* Divider */}
+
+
+ {/* Testimonials */}
+
+
+ {/* Divider */}
+
+
+ {/* Support Section */}
+
+
+ {/* Footer */}
+
);
diff --git a/src/components/landing/SpellShowcase.tsx b/src/components/landing/SpellShowcase.tsx
new file mode 100644
index 0000000..460bdf5
--- /dev/null
+++ b/src/components/landing/SpellShowcase.tsx
@@ -0,0 +1,175 @@
+import { useCallback, useEffect, useState } from "react";
+import useEmblaCarousel from "embla-carousel-react";
+import { ChevronLeft, ChevronRight, Sparkles } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+interface FeaturedSpell {
+ title: string;
+ description: string;
+ command: string;
+ preview: {
+ type: "feed" | "profile" | "stats";
+ content: string;
+ };
+}
+
+const FEATURED_SPELLS: FeaturedSpell[] = [
+ {
+ title: "Global Feed",
+ description: "See what's happening on Nostr right now",
+ command: "req -k 1 -l 20",
+ preview: {
+ type: "feed",
+ content: "Live stream of recent notes from across the Nostr network",
+ },
+ },
+ {
+ title: "Bitcoin News",
+ description: "Follow the latest Bitcoin discussions",
+ command: "req -k 1 -#t bitcoin -l 30",
+ preview: {
+ type: "feed",
+ content: "Curated feed of Bitcoin-related content",
+ },
+ },
+ {
+ title: "Long-form Articles",
+ description: "Deep dives and thought pieces",
+ command: "req -k 30023 -l 10",
+ preview: {
+ type: "feed",
+ content: "Discover in-depth articles from Nostr writers",
+ },
+ },
+ {
+ title: "Developer Activity",
+ description: "Track Nostr development and git repos",
+ command: "req -k 30617 -l 15",
+ preview: {
+ type: "feed",
+ content: "Monitor repository announcements and code updates",
+ },
+ },
+ {
+ title: "Network Stats",
+ description: "Count events across relays",
+ command: "count -k 1,6,7",
+ preview: {
+ type: "stats",
+ content: "Aggregate event counts for network insights",
+ },
+ },
+];
+
+export function SpellShowcase() {
+ const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
+ const [selectedIndex, setSelectedIndex] = useState(0);
+
+ const scrollPrev = useCallback(() => {
+ if (emblaApi) emblaApi.scrollPrev();
+ }, [emblaApi]);
+
+ const scrollNext = useCallback(() => {
+ if (emblaApi) emblaApi.scrollNext();
+ }, [emblaApi]);
+
+ const onSelect = useCallback(() => {
+ if (!emblaApi) return;
+ setSelectedIndex(emblaApi.selectedScrollSnap());
+ }, [emblaApi]);
+
+ useEffect(() => {
+ if (!emblaApi) return;
+ onSelect();
+ emblaApi.on("select", onSelect);
+ return () => {
+ emblaApi.off("select", onSelect);
+ };
+ }, [emblaApi, onSelect]);
+
+ return (
+
+
+
+
+ Featured Spells
+
+
+ Powerful queries to explore the Nostr network
+
+
+
+
+
+
+ {FEATURED_SPELLS.map((spell, index) => (
+
+
+
+
+ {spell.title}
+
+ {spell.command}
+
+
+ {spell.description}
+
+
+
+
+
+ {spell.preview.type}
+
+
+
+ {spell.preview.content}
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ {FEATURED_SPELLS.map((_, index) => (
+
+
+ );
+}
diff --git a/src/components/landing/SpellbookShowcase.tsx b/src/components/landing/SpellbookShowcase.tsx
new file mode 100644
index 0000000..aa8f2f0
--- /dev/null
+++ b/src/components/landing/SpellbookShowcase.tsx
@@ -0,0 +1,183 @@
+import { useCallback, useEffect, useState } from "react";
+import useEmblaCarousel from "embla-carousel-react";
+import { BookOpen, ChevronLeft, ChevronRight, Layout } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+interface FeaturedSpellbook {
+ title: string;
+ description: string;
+ author: string;
+ windows: string[];
+ useCase: string;
+}
+
+const FEATURED_SPELLBOOKS: FeaturedSpellbook[] = [
+ {
+ title: "Developer Dashboard",
+ description: "Complete setup for Nostr development and monitoring",
+ author: "fiatjaf",
+ windows: [
+ "NIP Documentation",
+ "Git Repositories Feed",
+ "Developer Chat",
+ "Network Stats",
+ ],
+ useCase: "Track PRs, NIPs, and coordinate with other devs",
+ },
+ {
+ title: "Bitcoin Research Hub",
+ description: "Follow Bitcoin news, analysis, and market sentiment",
+ author: "verbiricha",
+ windows: ["Bitcoin Feed", "Price Tracker", "Long-form Articles", "Charts"],
+ useCase: "Stay informed on Bitcoin ecosystem developments",
+ },
+ {
+ title: "Content Creator Studio",
+ description: "Manage your Nostr presence and engage with followers",
+ author: "jack",
+ windows: [
+ "Your Profile",
+ "Notifications",
+ "Mentions Feed",
+ "Scheduled Posts",
+ ],
+ useCase: "Create, publish, and monitor engagement",
+ },
+ {
+ title: "Community Manager",
+ description: "Moderate and engage with multiple Nostr groups",
+ author: "verbiricha",
+ windows: [
+ "Group Chat 1",
+ "Group Chat 2",
+ "Moderation Queue",
+ "Member Directory",
+ ],
+ useCase: "Coordinate communities and handle moderation",
+ },
+];
+
+export function SpellbookShowcase() {
+ const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
+ const [selectedIndex, setSelectedIndex] = useState(0);
+
+ const scrollPrev = useCallback(() => {
+ if (emblaApi) emblaApi.scrollPrev();
+ }, [emblaApi]);
+
+ const scrollNext = useCallback(() => {
+ if (emblaApi) emblaApi.scrollNext();
+ }, [emblaApi]);
+
+ const onSelect = useCallback(() => {
+ if (!emblaApi) return;
+ setSelectedIndex(emblaApi.selectedScrollSnap());
+ }, [emblaApi]);
+
+ useEffect(() => {
+ if (!emblaApi) return;
+ onSelect();
+ emblaApi.on("select", onSelect);
+ return () => {
+ emblaApi.off("select", onSelect);
+ };
+ }, [emblaApi, onSelect]);
+
+ return (
+
+
+
+
+ Featured Spellbooks
+
+
+ Complete workspace configurations for every use case
+
+
+
+
+
+
+ {FEATURED_SPELLBOOKS.map((spellbook, index) => (
+
+
+
+
+ {spellbook.title}
+
+ by @{spellbook.author}
+
+
+ {spellbook.description}
+
+
+
+
+
+
+ Layout Preview
+
+
+
+ {spellbook.windows.map((window, i) => (
+
+ {window}
+
+ ))}
+
+
+
+ Use case:{" "}
+ {spellbook.useCase}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ {FEATURED_SPELLBOOKS.map((_, index) => (
+
+
+ );
+}
diff --git a/src/components/landing/SupportSection.tsx b/src/components/landing/SupportSection.tsx
new file mode 100644
index 0000000..234aa4c
--- /dev/null
+++ b/src/components/landing/SupportSection.tsx
@@ -0,0 +1,171 @@
+import { useLiveQuery } from "dexie-react-hooks";
+import { Heart, Zap, Trophy, Users, TrendingUp } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Progress } from "@/components/ui/progress";
+import supportersService, { MONTHLY_GOAL_SATS } from "@/services/supporters";
+import { useProfile } from "@/hooks/useProfile";
+
+function SupporterCard({ pubkey, rank }: { pubkey: string; rank: number }) {
+ const profile = useProfile(pubkey);
+ const info = useLiveQuery(
+ () => supportersService.getSupporterInfo(pubkey),
+ [pubkey],
+ );
+
+ if (!info) return null;
+
+ const displayName = profile?.name || profile?.display_name || "Anonymous";
+ const avatar = profile?.picture;
+
+ const medals = ["🥇", "🥈", "🥉"];
+ const medal = rank < 3 ? medals[rank] : null;
+
+ return (
+
+
+ {medal && {medal}}
+ {avatar ? (
+
+ ) : (
+
+
+ {displayName.charAt(0).toUpperCase()}
+
+
+ )}
+
+ {displayName}
+
+ {info.zapCount} zap{info.zapCount !== 1 ? "s" : ""}
+
+
+
+
+
+ {info.totalSats.toLocaleString()}
+
+
+ );
+}
+
+export function SupportSection() {
+ const monthlyDonations = useLiveQuery(
+ () => supportersService.getMonthlyDonations(),
+ [],
+ );
+ const totalDonations = useLiveQuery(
+ () => supportersService.getTotalDonations(),
+ [],
+ );
+ const supporterCount = useLiveQuery(
+ () => supportersService.getSupporterCount(),
+ [],
+ );
+ const topSupporters = useLiveQuery(async () => {
+ const all = await supportersService.getAllSupporters();
+ return all.slice(0, 5); // Top 5
+ }, []);
+
+ const progressPercent = monthlyDonations
+ ? Math.min((monthlyDonations / MONTHLY_GOAL_SATS) * 100, 100)
+ : 0;
+
+ return (
+
+
+
+
+ Support Grimoire
+
+
+ Help us build the ultimate Nostr developer tool
+
+
+
+
+
+
+
+
+ Monthly Progress
+
+
+
+
+ {monthlyDonations?.toLocaleString() || 0}
+
+
+ / {MONTHLY_GOAL_SATS.toLocaleString()} sats goal
+
+
+
+
+
+
+
+
+
+ All-Time Support
+
+
+
+
+ {totalDonations?.toLocaleString() || 0}
+
+ sats raised
+
+
+
+
+
+
+
+ Supporters
+
+
+
+
+ {supporterCount?.toLocaleString() || 0}
+
+ contributors
+
+
+
+
+ {topSupporters && topSupporters.length > 0 && (
+
+
+
+
+ Top Contributors
+
+
+
+ {topSupporters.map((supporter, index) => (
+
+ ))}
+
+
+ )}
+
+
+
+
+ Lightning: grimoire@coinos.io
+
+
+
+ );
+}
diff --git a/src/components/landing/TestimonialsSection.tsx b/src/components/landing/TestimonialsSection.tsx
new file mode 100644
index 0000000..4097942
--- /dev/null
+++ b/src/components/landing/TestimonialsSection.tsx
@@ -0,0 +1,120 @@
+import { Heart, MessageSquare, Quote } from "lucide-react";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+} from "@/components/ui/card";
+
+export interface Testimonial {
+ author: {
+ name: string;
+ npub?: string;
+ avatar?: string;
+ };
+ content: string;
+ type: "note" | "highlight";
+ timestamp?: number;
+}
+
+interface TestimonialsSectionProps {
+ testimonials?: Testimonial[];
+}
+
+// Placeholder testimonials - will be replaced with real Nostr events
+const DEFAULT_TESTIMONIALS: Testimonial[] = [
+ {
+ author: {
+ name: "fiatjaf",
+ npub: "npub1...",
+ },
+ content:
+ "Grimoire is exactly what Nostr needed - a powerful developer tool that makes exploring the protocol intuitive and fun.",
+ type: "note",
+ },
+ {
+ author: {
+ name: "verbiricha",
+ npub: "npub1...",
+ },
+ content:
+ "The tiling window manager approach is genius. I can monitor multiple feeds, chat groups, and repositories all in one place.",
+ type: "note",
+ },
+ {
+ author: {
+ name: "jack",
+ npub: "npub1...",
+ },
+ content:
+ "Finally, a Nostr client that feels like a native Unix environment. The command palette is incredibly powerful.",
+ type: "highlight",
+ },
+];
+
+export function TestimonialsSection({
+ testimonials = DEFAULT_TESTIMONIALS,
+}: TestimonialsSectionProps) {
+ return (
+
+
+
+
+ What People Are Saying
+
+
+ Testimonials from the Nostr community
+
+
+
+
+ {testimonials.map((testimonial, index) => (
+
+
+
+
+ {testimonial.author.avatar ? (
+
+ ) : (
+
+
+ {testimonial.author.name.charAt(0).toUpperCase()}
+
+
+ )}
+
+
+ {testimonial.author.name}
+
+ {testimonial.author.npub && (
+
+ {testimonial.author.npub.slice(0, 12)}...
+
+ )}
+
+
+ {testimonial.type === "highlight" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {testimonial.content}
+
+
+
+ ))}
+
+
+ );
+}