From 80d36d5f163d781a2fd44f4a3aac6f1b57913aec Mon Sep 17 00:00:00 2001 From: Daniel Lam Date: Tue, 24 Mar 2026 17:26:34 +0700 Subject: [PATCH 01/16] add demo dropdown nav to landing page header Reads VITE_DEMO_URLS (comma-separated Label|URL pairs) and renders a dropdown menu in the landing page nav bar when configured. --- apps/app/.env.example | 4 ++ apps/app/src/config.ts | 6 +++ apps/app/src/index.css | 84 ++++++++++++++++++++++++++++++ apps/app/src/pages/LandingPage.tsx | 42 ++++++++++++++- 4 files changed, 135 insertions(+), 1 deletion(-) diff --git a/apps/app/.env.example b/apps/app/.env.example index bcb651fd..8eaaa605 100644 --- a/apps/app/.env.example +++ b/apps/app/.env.example @@ -16,6 +16,10 @@ VITE_MEMWAL_SERVER_URL=http://localhost:8000 # Docs URL (separate deployment) VITE_DOCS_URL=http://localhost:5174 +# Demo app links (comma-separated, format: Label|URL) +# Example: VITE_DEMO_URLS=Chat Demo|https://chat.example.com,Agent Demo|https://agent.example.com +VITE_DEMO_URLS= + # ══════════════════════════════════════════════════════════════ # ▷ INACTIVE: MAINNET (uncomment below, comment out TESTNET above) # ══════════════════════════════════════════════════════════════ diff --git a/apps/app/src/config.ts b/apps/app/src/config.ts index 4f510690..b14f7734 100644 --- a/apps/app/src/config.ts +++ b/apps/app/src/config.ts @@ -14,4 +14,10 @@ export const config = { .split(',').map(s => s.trim()).filter(Boolean) as string[], sidecarUrl: import.meta.env.VITE_SIDECAR_URL as string || 'http://localhost:9000', docsUrl: import.meta.env.VITE_DOCS_URL as string || '', + demoUrls: (import.meta.env.VITE_DEMO_URLS as string || '') + .split(',').map(s => s.trim()).filter(Boolean) + .map(entry => { + const [label, url] = entry.split('|').map(s => s.trim()) + return url ? { label, url } : { label: label, url: label } + }), } as const diff --git a/apps/app/src/index.css b/apps/app/src/index.css index 80ae5753..abd7d38d 100644 --- a/apps/app/src/index.css +++ b/apps/app/src/index.css @@ -515,6 +515,90 @@ h1, h2, h3 { box-shadow: 1px 1px 0 #000000; } +/* ── Demo Dropdown ── */ + +.lp-demo-dropdown { + position: relative; +} + +.lp-demo-trigger { + display: flex; + align-items: center; + gap: 4px; + background: none; + border: 2px solid #000000; + border-radius: 12px; + padding: 10px 18px; + font-size: 0.92rem; + font-weight: 700; + font-family: var(--font-sans); + cursor: pointer; + transition: transform 0.15s, box-shadow 0.15s; + box-shadow: 3px 3px 0 #000000; + color: #000000; + background: #ffffff; +} + +.lp-demo-trigger:hover { + transform: translate(-2px, -2px); + box-shadow: 5px 5px 0 #000000; +} + +.lp-demo-trigger:active { + transform: translate(2px, 2px); + box-shadow: 1px 1px 0 #000000; +} + +.lp-demo-chevron { + transition: transform 0.2s; +} + +.lp-demo-chevron.open { + transform: rotate(180deg); +} + +.lp-demo-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 200px; + background: #ffffff; + border: 2px solid #000000; + border-radius: 12px; + box-shadow: 4px 4px 0 #000000; + padding: 6px; + z-index: 100; + animation: lp-dropdown-in 0.15s ease-out; +} + +@keyframes lp-dropdown-in { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.lp-demo-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + color: #000000; + text-decoration: none; + font-size: 0.88rem; + font-weight: 600; + border-radius: 8px; + transition: background 0.1s; +} + +.lp-demo-item:hover { + background: #E8FF75; +} + /* ── Hero ── */ .lp-hero { diff --git a/apps/app/src/pages/LandingPage.tsx b/apps/app/src/pages/LandingPage.tsx index 2b9b6296..80ba3715 100644 --- a/apps/app/src/pages/LandingPage.tsx +++ b/apps/app/src/pages/LandingPage.tsx @@ -9,7 +9,8 @@ import { useWallets, } from '@mysten/dapp-kit' import { isEnokiWallet, type EnokiWallet, type AuthProvider } from '@mysten/enoki' -import { Github } from 'lucide-react' +import { ChevronDown, Github } from 'lucide-react' +import { useRef, useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { config } from '../config' import memwalLogo from '../assets/memwal-logo.svg' @@ -29,6 +30,19 @@ export default function LandingPage() { const navigate = useNavigate() const hasEnokiConfig = config.enokiApiKey && config.googleClientId + const demoUrls = config.demoUrls + const [demoOpen, setDemoOpen] = useState(false) + const demoRef = useRef(null) + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (demoRef.current && !demoRef.current.contains(e.target as Node)) { + setDemoOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) const handleConnect = () => { if (currentAccount) { @@ -49,6 +63,32 @@ export default function LandingPage() {
+ {demoUrls.length > 0 && ( +
+ + {demoOpen && ( +
+ {demoUrls.map(({ label, url }) => ( + setDemoOpen(false)} + > + {label} + + ))} +
+ )} +
+ )} {currentAccount ? (