a tool for shared writing and social publishing
0

Configure Feed

Select the types of activity you want to include in your feed.

add demo to landing page

Jared Pereira (May 26, 2026, 10:56 PM EDT) 222fefb5 2a044342

+158 -2
+11 -2
app/about/AboutPage.tsx
··· 1 1 import "./about.css"; 2 2 import { IBM_Plex_Serif } from "next/font/google"; 3 3 import { LandingCTA, LandingCTABottom } from "./LandingCTA"; 4 - import { Demo } from "./Demo"; 4 + import { InteractiveAppDemo } from "./InteractiveAppDemo"; 5 + import { InteractiveAppDemoMobile } from "./InteractiveAppDemoMobile"; 6 + import { Media } from "components/Media"; 5 7 import { Features } from "./Features"; 6 8 import { Examples } from "./Examples"; 7 9 import { Pricing } from "./Pricing"; ··· 45 47 community — whether you're writing a blog, newsletter, or secret third 46 48 thing. 47 49 </p> 48 - {/*<Demo />*/} 50 + <div className="pt-12"> 51 + <Media mobile={false}> 52 + <InteractiveAppDemo /> 53 + </Media> 54 + <Media mobile={true}> 55 + <InteractiveAppDemoMobile /> 56 + </Media> 57 + </div> 49 58 <Features /> 50 59 <Examples /> 51 60 <Pricing />
+80
app/about/InteractiveAppDemo.tsx
··· 1 + "use client"; 2 + import { useEffect, useState } from "react"; 3 + 4 + type Page = "drafts" | "posts" | "analytics" | "settings"; 5 + 6 + type Hit = { 7 + id: Page; 8 + label: string; 9 + topPct: number; 10 + leftPct: number; 11 + }; 12 + 13 + const HIT_WIDTH_PCT = 16.33; 14 + const HIT_HEIGHT_PCT = 2.8; 15 + const AUTOPLAY_INTERVAL_MS = 5000; 16 + 17 + const PAGES: Hit[] = [ 18 + { id: "drafts", label: "Drafts", topPct: 14.83, leftPct: 2.28 }, 19 + { id: "posts", label: "Posts", topPct: 18.0, leftPct: 2.28 }, 20 + { id: "analytics", label: "Analytics", topPct: 23.95, leftPct: 2.28 }, 21 + { id: "settings", label: "Settings", topPct: 27.15, leftPct: 2.28 }, 22 + ]; 23 + 24 + export function InteractiveAppDemo() { 25 + let [page, setPage] = useState<Page>("drafts"); 26 + let [autoplay, setAutoplay] = useState(true); 27 + let [animate, setAnimate] = useState(true); 28 + 29 + useEffect(() => { 30 + if (!autoplay) return; 31 + const interval = setInterval(() => { 32 + setAnimate(true); 33 + setPage((p) => { 34 + const idx = PAGES.findIndex((x) => x.id === p); 35 + return PAGES[(idx + 1) % PAGES.length].id; 36 + }); 37 + }, AUTOPLAY_INTERVAL_MS); 38 + return () => clearInterval(interval); 39 + }, [autoplay]); 40 + 41 + const handleClick = (id: Page) => { 42 + setAnimate(false); 43 + setAutoplay(false); 44 + setPage(id); 45 + }; 46 + 47 + return ( 48 + <div className="relative w-full max-w-[900px] mx-auto aspect-[2249/1798] rounded-lg shadow-md overflow-hidden"> 49 + {PAGES.map((p) => ( 50 + <img 51 + key={p.id} 52 + src={`/about/landing-${p.id}.webp`} 53 + alt={p.id === page ? `Leaflet ${p.label} page` : ""} 54 + aria-hidden={p.id !== page} 55 + className="absolute inset-0 w-full h-full" 56 + style={{ 57 + opacity: page === p.id ? 1 : 0, 58 + transition: animate ? "opacity 700ms ease-in-out" : "none", 59 + }} 60 + /> 61 + ))} 62 + {PAGES.map((p) => ( 63 + <button 64 + key={p.id} 65 + type="button" 66 + aria-label={`Show ${p.label}`} 67 + aria-current={page === p.id ? "page" : undefined} 68 + onClick={() => handleClick(p.id)} 69 + className="absolute cursor-pointer rounded-sm" 70 + style={{ 71 + top: `${p.topPct}%`, 72 + left: `${p.leftPct}%`, 73 + width: `${HIT_WIDTH_PCT}%`, 74 + height: `${HIT_HEIGHT_PCT}%`, 75 + }} 76 + /> 77 + ))} 78 + </div> 79 + ); 80 + }
+67
app/about/InteractiveAppDemoMobile.tsx
··· 1 + "use client"; 2 + import { useEffect, useState } from "react"; 3 + import { ToggleGroup } from "components/ToggleGroup"; 4 + 5 + type Page = "drafts" | "posts" | "analytics" | "settings"; 6 + 7 + const PAGES: { id: Page; label: string }[] = [ 8 + { id: "drafts", label: "Drafts" }, 9 + { id: "posts", label: "Posts" }, 10 + { id: "analytics", label: "Analytics" }, 11 + { id: "settings", label: "Settings" }, 12 + ]; 13 + 14 + const AUTOPLAY_INTERVAL_MS = 5000; 15 + 16 + export function InteractiveAppDemoMobile() { 17 + let [page, setPage] = useState<Page>("drafts"); 18 + let [autoplay, setAutoplay] = useState(true); 19 + let [animate, setAnimate] = useState(true); 20 + 21 + useEffect(() => { 22 + if (!autoplay) return; 23 + const interval = setInterval(() => { 24 + setAnimate(true); 25 + setPage((p) => { 26 + const idx = PAGES.findIndex((x) => x.id === p); 27 + return PAGES[(idx + 1) % PAGES.length].id; 28 + }); 29 + }, AUTOPLAY_INTERVAL_MS); 30 + return () => clearInterval(interval); 31 + }, [autoplay]); 32 + 33 + const handleClick = (id: Page) => { 34 + setAnimate(false); 35 + setAutoplay(false); 36 + setPage(id); 37 + }; 38 + 39 + return ( 40 + <div className="flex flex-col gap-3 w-full max-w-[320px] mx-auto"> 41 + <ToggleGroup 42 + fullWidth 43 + className="bg-[#F1EDE5]! w-fill mx-auto" 44 + selectedOptionClassName="bg-[#686153]! text-white!" 45 + optionClassName="text-[#969696]! text-sm" 46 + value={page} 47 + onChange={handleClick} 48 + options={PAGES.map((p) => ({ value: p.id, label: p.label }))} 49 + /> 50 + <div className="relative w-full aspect-[752/1334] rounded-lg shadow-md overflow-hidden"> 51 + {PAGES.map((p) => ( 52 + <img 53 + key={p.id} 54 + src={`/about/mobile-landing-${p.id}.webp`} 55 + alt={p.id === page ? `Leaflet ${p.label} page` : ""} 56 + aria-hidden={p.id !== page} 57 + className="absolute inset-0 w-full h-full" 58 + style={{ 59 + opacity: page === p.id ? 1 : 0, 60 + transition: animate ? "opacity 700ms ease-in-out" : "none", 61 + }} 62 + /> 63 + ))} 64 + </div> 65 + </div> 66 + ); 67 + }
public/about/landing-analytics.webp

This is a binary file and will not be displayed.

public/about/landing-drafts.webp

This is a binary file and will not be displayed.

public/about/landing-posts.webp

This is a binary file and will not be displayed.

public/about/landing-settings.webp

This is a binary file and will not be displayed.

public/about/mobile-landing-analytics.webp

This is a binary file and will not be displayed.

public/about/mobile-landing-drafts.webp

This is a binary file and will not be displayed.

public/about/mobile-landing-posts.webp

This is a binary file and will not be displayed.

public/about/mobile-landing-settings.webp

This is a binary file and will not be displayed.