Skip to content
Merged
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
78 changes: 56 additions & 22 deletions src/components/lineage/LineageCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { GitFork, GitBranch, Percent } from "lucide-react";
import { supabase } from "@/integrations/supabase/client";

interface ParentInfo {
Expand All @@ -13,7 +14,8 @@ interface ChildSummary {
}

/**
* Renders lineage attribution for a package:
* Renders lineage attribution for a package — visible proof of the
* revenue-share moat:
* - if this package was forked, link back to upstream and disclose rev-share
* - if other packages forked this one, surface descendant count
*/
Expand All @@ -27,7 +29,9 @@ export function LineageCard({ packageId }: { packageId: string }) {
const [pq, cq] = await Promise.all([
supabase
.from("package_lineage")
.select("fork_kind, rev_share_bps, parent_package_id, packages!package_lineage_parent_package_id_fkey(slug,name)")
.select(
"fork_kind, rev_share_bps, parent_package_id, packages!package_lineage_parent_package_id_fkey(slug,name)",
)
.eq("child_package_id", packageId)
.maybeSingle(),
supabase
Expand All @@ -37,7 +41,11 @@ export function LineageCard({ packageId }: { packageId: string }) {
]);
if (cancel) return;
const parentRow = pq.data as
| { fork_kind: string; rev_share_bps: number; packages: { slug: string; name: string } | null }
| {
fork_kind: string;
rev_share_bps: number;
packages: { slug: string; name: string } | null;
}
| null;
if (parentRow?.packages) {
setParent({
Expand All @@ -49,30 +57,56 @@ export function LineageCard({ packageId }: { packageId: string }) {
}
setChildren({ count: cq.count ?? 0 });
})();
return () => { cancel = true; };
return () => {
cancel = true;
};
}, [packageId]);

if (!parent && (!children || children.count === 0)) return null;

return (
<aside className="border rounded-lg p-4 bg-muted/30 my-4">
<h3 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground mb-2">Lineage</h3>
{parent && (
<p className="text-sm">
<span className="font-medium capitalize">{parent.fork_kind}</span> of{" "}
<a href={`/packs/${parent.slug}`} className="underline">{parent.name}</a>.
{" "}Upstream receives{" "}
<span className="font-mono">{(parent.rev_share_bps / 100).toFixed(2)}%</span> of every sale.
</p>
)}
{children && children.count > 0 && (
<p className="text-sm mt-2">
<a href={`/packs/${packageId}/forks`} className="underline">
{children.count} {children.count === 1 ? "fork" : "forks"}
</a>{" "}
downstream contribute revenue back here.
</p>
)}
<aside className="my-6 overflow-hidden rounded-xl border border-border bg-surface/40">
<header className="flex items-center gap-2 border-b border-border px-4 py-2.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
<GitFork className="h-3.5 w-3.5" strokeWidth={2} aria-hidden />
Lineage
</header>
<div className="divide-y divide-border">
{parent && (
<div className="flex items-start gap-3 px-4 py-3">
<span className="mt-0.5 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
<GitBranch className="h-3.5 w-3.5" strokeWidth={2} aria-hidden />
</span>
<div className="min-w-0 flex-1">
<p className="text-sm">
<span className="font-medium capitalize">{parent.fork_kind}</span> of{" "}
<a
href={`/packs/${parent.slug}`}
className="font-medium text-primary underline-offset-4 hover:underline"
>
{parent.name}
</a>
</p>
<p className="mt-1 inline-flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:text-emerald-400">
<Percent className="h-3 w-3" strokeWidth={2.5} aria-hidden />
{(parent.rev_share_bps / 100).toFixed(2)}% goes to upstream author
</p>
</div>
</div>
)}
{children && children.count > 0 && (
<div className="flex items-start gap-3 px-4 py-3">
<span className="mt-0.5 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
<GitFork className="h-3.5 w-3.5" strokeWidth={2} aria-hidden />
</span>
<p className="text-sm">
<a href={`/packs/${packageId}/forks`} className="font-medium text-primary underline-offset-4 hover:underline">
{children.count} {children.count === 1 ? "fork" : "forks"}
</a>{" "}
downstream pay revenue back to this package.
</p>
</div>
)}
</div>
</aside>
);
}
37 changes: 37 additions & 0 deletions src/components/site/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ReactNode } from "react";
import type { LucideIcon } from "lucide-react";
import { Inbox } from "lucide-react";

/**
* Friendly empty state. Defaults to an Inbox glyph but accepts any
* lucide icon. Optional CTA renders as a primary button or link.
*/
export function EmptyState({
icon: Icon = Inbox,
title,
body,
cta,
}: {
icon?: LucideIcon;
title: string;
body?: ReactNode;
cta?: { label: string; href: string };
}) {
return (
<div className="mx-auto flex max-w-md flex-col items-center rounded-2xl border border-dashed border-border bg-surface/30 px-6 py-10 text-center">
<div className="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10 text-primary">
<Icon className="h-5 w-5" strokeWidth={1.75} aria-hidden />
</div>
<h3 className="mt-4 text-lg font-semibold tracking-tight">{title}</h3>
{body && <p className="mt-2 text-sm text-muted-foreground">{body}</p>}
{cta && (
<a
href={cta.href}
className="mt-5 inline-flex h-10 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:opacity-95"
>
{cta.label}
</a>
)}
</div>
);
}
9 changes: 8 additions & 1 deletion src/components/site/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function Footer() {
<NewsletterForm />
</div>

<div className="grid grid-cols-2 gap-10 md:col-span-7 md:grid-cols-4">
<div className="grid grid-cols-2 gap-10 md:col-span-7 md:grid-cols-5">
<FooterCol title="Product" links={[
{ label: "Connect", to: "/connect" },
{ label: "Marketplace", to: "/marketplace" },
Expand All @@ -38,6 +38,13 @@ export function Footer() {
{ label: "Evaluation", to: "/evaluation" },
{ label: "Rankings", to: "/marketplace/rankings" },
]} />
<FooterCol title="Community" links={[
{ label: "Skill of the Week", to: "/skill-of-the-week" },
{ label: "Use cases", to: "/use-cases" },
{ label: "Bounties", to: "/bounties" },
{ label: "Affiliate leaderboard", to: "/leaderboard" },
{ label: "Discord", href: "https://www.superagentskill.com/community" },
]} />
<FooterCol title="Open source" links={[
{ label: "GitHub repo", href: "https://github.com/criptogus/agent-evolve-network" },
{ label: "Contribute", to: "/community" },
Expand Down
11 changes: 11 additions & 0 deletions src/components/site/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Link } from "@tanstack/react-router";
import { useState } from "react";
import { Menu, Github, ChevronDown } from "lucide-react";
import { Logo } from "./Logo";
import { ThemeToggle } from "./ThemeToggle";
import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetHeader } from "@/components/ui/sheet";
import {
DropdownMenu,
Expand Down Expand Up @@ -34,6 +35,13 @@ const CREATE: NavItem[] = [
{ to: "/match", label: "Match", hint: "Pair skills to a goal" },
];

const COMMUNITY: NavItem[] = [
{ to: "/skill-of-the-week", label: "Skill of the Week", hint: "Top skill, weekly" },
{ to: "/use-cases", label: "Use cases", hint: "By vertical & task" },
{ to: "/bounties", label: "Bounties", hint: "Get paid to publish skills" },
{ to: "/leaderboard", label: "Affiliate leaderboard", hint: "Top referrers (30d)" },
];

const SIMPLE: NavItem[] = [
{ to: "/connect", label: "Connect" },
{ to: "/pricing", label: "Pricing" },
Expand Down Expand Up @@ -84,6 +92,7 @@ export function Nav() {
<nav className="hidden min-w-0 items-center gap-5 lg:flex">
<NavDropdown label="Browse" items={BROWSE} />
<NavDropdown label="Create" items={CREATE} />
<NavDropdown label="Community" items={COMMUNITY} />
{SIMPLE.map((l) => (
<Link
key={l.to}
Expand All @@ -96,6 +105,7 @@ export function Nav() {
</nav>
</div>
<div className="flex shrink-0 items-center gap-2">
<ThemeToggle className="hidden h-8 w-8 sm:inline-flex" />
<a
href={GITHUB_URL}
target="_blank"
Expand Down Expand Up @@ -173,6 +183,7 @@ export function Nav() {
<nav className="mt-6 flex flex-col gap-4">
<MobileSection title="Browse" items={BROWSE} onNavigate={() => setOpen(false)} />
<MobileSection title="Create" items={CREATE} onNavigate={() => setOpen(false)} />
<MobileSection title="Community" items={COMMUNITY} onNavigate={() => setOpen(false)} />
<MobileSection title="More" items={SIMPLE} onNavigate={() => setOpen(false)} />

<div className="mt-2 border-t border-border pt-4">
Expand Down
22 changes: 22 additions & 0 deletions src/components/site/SitePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ReactNode } from "react";
import { Nav } from "./Nav";
import { Footer } from "./Footer";

/**
* Standard page chrome: Nav + main content + Footer. Use for every route
* outside the marketing landing page so users never land on an "orphan"
* screen without branding or navigation.
*
* <SitePage>
* <main className="...">…</main>
* </SitePage>
*/
export function SitePage({ children }: { children: ReactNode }) {
return (
<div className="flex min-h-screen flex-col bg-background">
<Nav />
<div className="flex-1">{children}</div>
<Footer />
</div>
);
}
44 changes: 44 additions & 0 deletions src/components/site/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
import { Sun, Moon } from "lucide-react";

type Mode = "light" | "dark";

function readMode(): Mode {
if (typeof document === "undefined") return "light";
const stored = localStorage.getItem("sas-theme");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard theme read against blocked storage access

localStorage.getItem("sas-theme") is called without a try/catch. In browsers/environments where storage access is blocked (e.g., strict privacy settings), this can throw a SecurityError during the effect path and break the toggle initialization inside the navigation UI.

Useful? React with 👍 / 👎.

if (stored === "light" || stored === "dark") return stored;
return document.documentElement.classList.contains("dark") ? "dark" : "light";
}

function applyMode(mode: Mode) {
document.documentElement.classList.toggle("dark", mode === "dark");
try { localStorage.setItem("sas-theme", mode); } catch {}
}

export function ThemeToggle({ className = "" }: { className?: string }) {
const [mode, setMode] = useState<Mode>("light");

useEffect(() => {
setMode(readMode());
}, []);
Comment on lines +21 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply stored theme to document on initial load

The mount effect only updates React state (setMode(readMode())) and never applies that mode to <html>, so a previously saved dark preference in localStorage is not actually restored after a full page reload. In that scenario the icon flips to dark-mode state, but the page remains in light theme until the user toggles again, which breaks the advertised persistence behavior.

Useful? React with 👍 / 👎.


const toggle = () => {
const next: Mode = mode === "dark" ? "light" : "dark";
setMode(next);
applyMode(next);
};

const Icon = mode === "dark" ? Sun : Moon;

return (
<button
type="button"
onClick={toggle}
aria-label={`Switch to ${mode === "dark" ? "light" : "dark"} theme`}
title={`Switch to ${mode === "dark" ? "light" : "dark"} theme`}
className={`inline-flex h-9 w-9 items-center justify-center rounded-md border border-border bg-background/40 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground ${className}`}
>
<Icon className="h-4 w-4" strokeWidth={1.75} />
</button>
);
}
Loading
Loading