Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/components/continue-learning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useEffect, useState } from "react";
import { ArrowRight } from "lucide-react";
import { getLearnProgress, type LearnProgress } from "@/lib/learn-progress";
import { cn } from "@/lib/utils";

/**
* Renders only when localStorage already has a learn path.
* First visit stays empty — never a decorative Continue chip.
*/
export function ContinueLearningChip({
hideIfPath,
className,
}: {
hideIfPath?: string;
className?: string;
}) {
const [progress, setProgress] = useState<LearnProgress | null>(null);

useEffect(() => {
const next = getLearnProgress();
if (next && hideIfPath && next.path === hideIfPath) {
setProgress(null);
return;
}
setProgress(next);
}, [hideIfPath]);

if (!progress) return null;

return (
<a
href={progress.path}
className={cn(
"chip inline-flex items-center gap-2 border border-border bg-surface px-3 py-1.5 text-sm font-medium text-fg no-underline hover:border-primary/40",
className,
)}
>
Continue Learning
<span className="font-normal text-muted">· {progress.title}</span>
<ArrowRight className="size-3.5" />
</a>
);
}
45 changes: 17 additions & 28 deletions src/components/site-chrome.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import type { ReactNode } from "react";
import { useEffect, type ReactNode } from "react";
import { Link } from "@tanstack/react-router";
import { BrandMark } from "@/components/brand-mark";
import { SupportNudge } from "@/components/support-nudge";
import { BRAND, NAV_LINKS } from "@/lib/brand";
import { markLearnProgress } from "@/lib/learn-progress";
import { LEARNING_PATH } from "@/lib/learning-path";
import { cn } from "@/lib/utils";

/** Marketing / docs chrome — shipx402.com product site */
export function SiteChrome({
children,
activePath,
}: {
children: ReactNode;
activePath?: string;
}) {
export function SiteChrome({ children, activePath }: { children: ReactNode; activePath?: string }) {
const guideLinks = LEARNING_PATH.filter((i) => i.path.startsWith("/guides/"));

useEffect(() => {
if (activePath) markLearnProgress(activePath);
}, [activePath]);

return (
<div className="min-h-dvh bg-bg text-fg">
<a
Expand Down Expand Up @@ -45,19 +44,18 @@ export function SiteChrome({
<Link to="/" className="no-underline" aria-label="Ship x402 home">
<BrandMark showDomain size="md" />
</Link>
<nav
className="flex flex-wrap items-center gap-0.5 sm:gap-1"
aria-label="Primary"
>
<nav className="flex flex-wrap items-center gap-0.5 sm:gap-1" aria-label="Primary">
{NAV_LINKS.map((link) => {
const hash = "hash" in link ? link.hash : undefined;
const active =
activePath === link.href ||
(link.href === "/learn" &&
!!activePath?.startsWith("/guides/"));
!hash &&
(activePath === link.href ||
(link.href === "/learn" && !!activePath?.startsWith("/guides/")));
return (
<Link
key={link.href}
key={`${link.href}${hash ? `#${hash}` : ""}`}
to={link.href}
hash={hash}
className={cn(
"chip px-3 py-2 text-sm font-medium no-underline transition-colors",
active
Expand Down Expand Up @@ -151,26 +149,17 @@ export function SiteChrome({
</a>
</li>
<li>
<a
href="/api/agents/curriculum"
className="text-muted hover:text-fg"
>
<a href="/api/agents/curriculum" className="text-muted hover:text-fg">
Curriculum API
</a>
</li>
<li>
<a
href="/api/agents/site"
className="text-muted hover:text-fg"
>
<a href="/api/agents/site" className="text-muted hover:text-fg">
Catalog JSON
</a>
</li>
<li>
<a
href="/.well-known/agent-card.json"
className="text-muted hover:text-fg"
>
<a href="/.well-known/agent-card.json" className="text-muted hover:text-fg">
agent-card.json
</a>
</li>
Expand Down
113 changes: 113 additions & 0 deletions src/components/wallet-setup-strip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Link } from "@tanstack/react-router";
import { ArrowRight, Wallet } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { WALLET_INSTALL } from "@/lib/brand";

/** Video Concept v1.1 — frozen, do not paraphrase. */
export const WALLET_FAST_COPY = {
title: "Make a Wallet Fast",
deck: "Pick a wallet. Start on Devnet. We never ask for your seed.",
bridge: "Then walk the Loop — free cert.",
alreadyHave: "I already have a wallet",
howKeys: "How keys work",
practice: "Practice",
continueDevnet: "Continue on Devnet",
phantom: "Phantom",
solflare: "Solflare",
installPhantom: "Install Phantom",
installSolflare: "Install Solflare",
devnetChip: "Devnet First",
} as const;

function DevnetChip() {
return (
<span className="chip inline-flex items-center border border-primary/35 bg-bg/70 px-2.5 py-0.5 text-[11px] font-semibold tracking-wide text-primary">
{WALLET_FAST_COPY.devnetChip}
</span>
);
}

export function WalletSetupStrip() {
return (
<section
id="setup"
className="relative mx-auto w-full max-w-3xl scroll-mt-28 overflow-hidden rounded-[var(--radius-xl)] bg-surface rgb-frame"
>
<span id="wallet-fast" className="absolute top-0" />
<div className="relative z-10 space-y-4 px-5 py-6 text-center sm:px-8 sm:py-7">
<div className="flex flex-wrap items-center justify-center gap-2.5">
<Wallet className="size-5 text-primary" aria-hidden />
<h2 className="text-xl font-semibold tracking-tight text-fg sm:text-2xl">
{WALLET_FAST_COPY.title}
</h2>
<DevnetChip />
</div>
<p className="text-base leading-relaxed text-muted">{WALLET_FAST_COPY.deck}</p>

<Tabs defaultValue="practice" className="mx-auto max-w-lg">
<TabsList className="w-full justify-center">
<TabsTrigger value="practice">{WALLET_FAST_COPY.practice}</TabsTrigger>
<TabsTrigger value="phantom">{WALLET_FAST_COPY.phantom}</TabsTrigger>
<TabsTrigger value="solflare">{WALLET_FAST_COPY.solflare}</TabsTrigger>
</TabsList>

<TabsContent value="practice" className="space-y-3">
<div className="flex flex-wrap items-center justify-center gap-2">
<Button asChild>
<Link to="/app">
{WALLET_FAST_COPY.practice}
<ArrowRight className="size-4" />
</Link>
</Button>
</div>
</TabsContent>

<TabsContent value="phantom" className="space-y-3">
<div className="flex flex-wrap items-center justify-center gap-2">
<Button asChild>
<a href={WALLET_INSTALL.phantom.installUrl} target="_blank" rel="noreferrer">
{WALLET_FAST_COPY.installPhantom}
<ArrowRight className="size-4" />
</a>
</Button>
<Button asChild variant="secondary">
<Link to="/loop">
{WALLET_FAST_COPY.continueDevnet}
<ArrowRight className="size-4" />
</Link>
</Button>
</div>
</TabsContent>

<TabsContent value="solflare" className="space-y-3">
<div className="flex flex-wrap items-center justify-center gap-2">
<Button asChild>
<a href={WALLET_INSTALL.solflare.installUrl} target="_blank" rel="noreferrer">
{WALLET_FAST_COPY.installSolflare}
<ArrowRight className="size-4" />
</a>
</Button>
<Button asChild variant="secondary">
<Link to="/loop">
{WALLET_FAST_COPY.continueDevnet}
<ArrowRight className="size-4" />
</Link>
</Button>
</div>
</TabsContent>
</Tabs>

<p className="text-sm text-muted">{WALLET_FAST_COPY.bridge}</p>
<div className="flex flex-wrap items-center justify-center gap-x-5 gap-y-2">
<Link to="/loop" className="link-readable text-sm font-medium">
{WALLET_FAST_COPY.alreadyHave}
</Link>
<Link to="/wallet" className="link-readable text-sm font-medium">
{WALLET_FAST_COPY.howKeys}
</Link>
</div>
</div>
</section>
);
}
22 changes: 15 additions & 7 deletions src/lib/brand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,7 @@ export const SEO_PAGES: Record<string, SeoPage> = {
description:
"Interactive walkthrough: see a live 402, read the price tag, dry-run the client loop, pass a knowledge check, and earn a free educational certificate. Tips optional.",
h1: "Walk the x402 Payment Loop",
keywords:
"x402 payment loop, HTTP 402 tutorial, free certificate, agent payments walkthrough",
keywords: "x402 payment loop, HTTP 402 tutorial, free certificate, agent payments walkthrough",
},
about: {
path: "/about",
Expand Down Expand Up @@ -206,8 +205,7 @@ export const SEO_PAGES: Record<string, SeoPage> = {
description:
"Yes, with guardrails. How AI agents pay for APIs and services per request using x402, what that really means, and the limits you set yourself.",
h1: "Can AI agents actually spend money now?",
keywords:
"can AI agents spend money, agent payments, autonomous agent payments, x402 agents",
keywords: "can AI agents spend money, agent payments, autonomous agent payments, x402 agents",
},
testX402Endpoint: {
path: "/guides/test-x402-endpoint",
Expand Down Expand Up @@ -292,13 +290,23 @@ export const SEO_PAGES: Record<string, SeoPage> = {
},
};

/** Official wallet install pages only — never seed-entry or import UIs. */
export const WALLET_INSTALL = {
phantom: {
name: "Phantom",
installUrl: "https://phantom.com/download",
},
solflare: {
name: "Solflare",
installUrl: "https://www.solflare.com/download",
},
} as const;

export const NAV_LINKS = [
{ href: "/", hash: "setup", label: "Get Set Up" },
{ href: "/learn", label: "Learn" },
{ href: "/loop", label: "Loop" },
{ href: "/tools", label: "Tools" },
{ href: "/ship", label: "Ship" },
{ href: "/check", label: "Check" },
{ href: "/explorer", label: "Balance" },
{ href: "/agents", label: "Agents" },
{ href: "/donate", label: "Donate" },
] as const;
Expand Down
75 changes: 75 additions & 0 deletions src/lib/learn-progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Client-only learning progress for Home / Learn "Continue" chips.
* Never stores keys, seeds, or wallet secrets — path + title only.
*/

import { LEARNING_PATH } from "@/lib/learning-path";

export type LearnProgress = {
path: string;
title: string;
updatedAt: number;
};

export const LEARN_PROGRESS_KEY = "shipx402.learnProgress.v1";

const TRACKED_EXACT = new Set(["/loop", "/wallet"]);

function isTrackedPath(path: string): boolean {
return TRACKED_EXACT.has(path) || path.startsWith("/guides/");
}

export function titleForLearnPath(path: string): string {
const fromPath = LEARNING_PATH.find((item) => item.path === path);
if (fromPath) return fromPath.title;
switch (path) {
case "/learn":
return "Learn Path";
case "/loop":
return "Walk the Loop";
case "/wallet":
return "How Keys Work";
default:
return "Continue Learning";
}
}

export function getLearnProgress(): LearnProgress | null {
if (typeof window === "undefined") return null;
try {
const raw = window.localStorage.getItem(LEARN_PROGRESS_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw) as Partial<LearnProgress>;
if (
typeof parsed.path !== "string" ||
!parsed.path.startsWith("/") ||
typeof parsed.title !== "string" ||
typeof parsed.updatedAt !== "number"
) {
return null;
}
if (!isTrackedPath(parsed.path)) return null;
return {
path: parsed.path,
title: parsed.title,
updatedAt: parsed.updatedAt,
};
} catch {
return null;
}
}

export function markLearnProgress(path: string, title?: string): void {
if (typeof window === "undefined") return;
if (!isTrackedPath(path)) return;
const record: LearnProgress = {
path,
title: title ?? titleForLearnPath(path),
updatedAt: Date.now(),
};
try {
window.localStorage.setItem(LEARN_PROGRESS_KEY, JSON.stringify(record));
} catch {
/* private mode / quota — Continue chips simply stay hidden */
}
}
Loading