diff --git a/designs/shirts/fungi-logo-tee/fungi-logo-tee.png b/designs/shirts/fungi-logo-tee/fungi-logo-tee.png deleted file mode 100644 index 1d917d2..0000000 Binary files a/designs/shirts/fungi-logo-tee/fungi-logo-tee.png and /dev/null differ diff --git a/designs/shirts/fungi-logo-tee/metadata.yaml b/designs/shirts/fungi-logo-tee/metadata.yaml deleted file mode 100644 index 5f49ba5..0000000 --- a/designs/shirts/fungi-logo-tee/metadata.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: "Fungi Flows Logo Tee" -slug: fungi-logo-tee -description: "The original Fungi Flows logo on a premium tee. Gold and bioluminescent green on deep purple — rep the mycelium movement. Revenue supports the Fungi Flows community." -tags: [fungiflows, mushroom, logo, hip-hop, pittsburgh, tee, community] -space: all -category: shirts -created: "2026-02-18" -author: fungi-flows - -source: - file: fungi-logo-tee.png - format: png - dimensions: - width: 3600 - height: 4800 - dpi: 300 - color_profile: sRGB - -products: - - type: shirt - provider: printful - sku: "71" - variants: [S, M, L, XL, 2XL] - retail_price: 29.99 - - type: hoodie - provider: printful - sku: "146" - variants: [S, M, L, XL, 2XL] - retail_price: 49.99 - -status: active diff --git a/designs/stickers/fungi-spore/fungi-spore.png b/designs/stickers/fungi-spore/fungi-spore.png deleted file mode 100644 index cff14af..0000000 Binary files a/designs/stickers/fungi-spore/fungi-spore.png and /dev/null differ diff --git a/designs/stickers/fungi-spore/metadata.yaml b/designs/stickers/fungi-spore/metadata.yaml deleted file mode 100644 index e707648..0000000 --- a/designs/stickers/fungi-spore/metadata.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: "Spore Print Sticker" -slug: fungi-spore -description: "Bioluminescent mushroom spore print design. Neon green on deep purple — the signature Fungi Flows aesthetic. Revenue supports the Fungi Flows community." -tags: [fungiflows, mushroom, spore, psychedelic, sticker, community] -space: all -category: stickers -created: "2026-02-18" -author: fungi-flows - -source: - file: fungi-spore.png - format: png - dimensions: - width: 1200 - height: 1200 - dpi: 300 - color_profile: sRGB - -products: - - type: sticker - provider: prodigi - sku: GLOBAL-STI-KIS-4X4 - variants: [matte, gloss] - retail_price: 4.99 - -status: active diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 43f3645..1c03dc6 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -5,6 +5,7 @@ import "./globals.css"; import type { SpaceConfig } from "@/lib/spaces"; import { themeToCSS } from "@/lib/spaces"; import { HeaderBar } from "@/components/HeaderBar"; +import { EcosystemFooter } from "@/components/EcosystemFooter"; const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api"; @@ -31,26 +32,6 @@ export async function generateMetadata(): Promise { }; } -const ECOSYSTEM_LINKS = [ - { name: "rSpace", href: "https://rspace.online" }, - { name: "rSwag", href: "https://rswag.online", active: true }, - { name: "rWork", href: "https://rwork.online" }, - { name: "rMaps", href: "https://rmaps.online" }, - { name: "rNotes", href: "https://rnotes.online" }, - { name: "rVote", href: "https://rvote.online" }, - { name: "rFunds", href: "https://rfunds.online" }, - { name: "rTrips", href: "https://rtrips.online" }, - { name: "rCart", href: "https://rcart.online" }, - { name: "rWallet", href: "https://rwallet.online" }, - { name: "rFiles", href: "https://rfiles.online" }, - { name: "rTube", href: "https://rtube.online" }, - { name: "rCal", href: "https://rcal.online" }, - { name: "rNetwork", href: "https://rnetwork.online" }, - { name: "rInbox", href: "https://rinbox.online" }, - { name: "rStack", href: "https://rstack.online" }, - { name: "rAuctions", href: "https://rauctions.online" }, - { name: "rPubs", href: "https://rpubs.online" }, -]; export default async function RootLayout({ children, @@ -84,31 +65,7 @@ export default async function RootLayout({
{children}
{/* ── Ecosystem Footer ────────────────────────────── */} - + diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 5ab0f07..be943b7 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { cookies } from "next/headers"; import type { SpaceConfig } from "@/lib/spaces"; +import { RevenueFlowSankey } from "@/components/RevenueFlowSankey"; const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api"; @@ -248,107 +249,43 @@ export default async function HomePage() { - {/* ── Community Revenue Model ───────────────────────────── */} + {/* ── Community Revenue Model — Interactive Sankey ─────── */}
-
-

+
+ + Revenue Model + +

Merch That Funds Your Mission

-

+

Every purchase feeds revenue directly into your community's - funding streams. No middlemen, no platform fees eating your margins. + funding streams. Drag the sliders to explore how the money flows.

-
- {/* Revenue Flow */} -
-
-
- - - -
-

Direct Revenue Stream

-
-

- Set your own margins. Printful handles production at cost, and - the markup goes straight into your community's treasury, DAO, - or project fund. -

-
-
- Production cost - $9.25 -
-
- Your price - $29.99 -
-
- Community revenue - $20.74 per sale -
-
-
+
+ +
- {/* Community Benefits */} -
-
-
- - - -
-

Built for Communities

-
-
    -
  • - - - - Branded Spaces — each community gets their own storefront with custom theme, logo, and catalog -
  • -
  • - - - - Zero inventory risk — items printed on demand, no upfront costs for your community -
  • -
  • - - - - Revenue routing — connect to rFunds, DAOs, or any wallet to stream merch revenue directly into community funding -
  • -
  • - - - - Local production — Printful fulfills from the nearest center, reducing shipping distance and carbon footprint -
  • -
+ {/* Key benefits row */} +
+
+
$0
+
Platform Fees
+
+
+
100%
+
Margin to Community
+
+
+
You
+
Set the Splits
+
+
+
0
+
Inventory Risk
diff --git a/frontend/components/AppSwitcher.tsx b/frontend/components/AppSwitcher.tsx index 4880e65..2544fca 100644 --- a/frontend/components/AppSwitcher.tsx +++ b/frontend/components/AppSwitcher.tsx @@ -2,12 +2,12 @@ import { useState, useRef, useEffect } from 'react'; -interface AppModule { +export interface AppModule { id: string; name: string; - badge: string; - color: string; - emoji: string; + badge: string; // favicon-style abbreviation: rS, rN, rP, etc. + color: string; // Tailwind bg class for the pastel badge + emoji: string; // function emoji shown right of title description: string; domain?: string; } @@ -16,27 +16,37 @@ const MODULES: AppModule[] = [ // Creating { id: 'space', name: 'rSpace', badge: 'rS', color: 'bg-teal-300', emoji: '🎨', description: 'Real-time collaborative canvas', domain: 'rspace.online' }, { id: 'notes', name: 'rNotes', badge: 'rN', color: 'bg-amber-300', emoji: '📝', description: 'Group note-taking & knowledge capture', domain: 'rnotes.online' }, - { id: 'pubs', name: 'rPubs', badge: 'rP', color: 'bg-rose-300', emoji: '📰', description: 'Collaborative publishing platform', domain: 'rpubs.online' }, + { id: 'pubs', name: 'rPubs', badge: 'rP', color: 'bg-rose-300', emoji: '📰', description: 'Collaborative publishing platform', domain: 'rpubs.online' }, // Planning { id: 'cal', name: 'rCal', badge: 'rC', color: 'bg-sky-300', emoji: '📅', description: 'Collaborative scheduling & events', domain: 'rcal.online' }, { id: 'trips', name: 'rTrips', badge: 'rT', color: 'bg-emerald-300', emoji: '✈️', description: 'Group travel planning in real time', domain: 'rtrips.online' }, { id: 'maps', name: 'rMaps', badge: 'rM', color: 'bg-green-300', emoji: '🗺️', description: 'Collaborative real-time mapping', domain: 'rmaps.online' }, - // Discussing & Deciding - { id: 'inbox', name: 'rInbox', badge: 'rI', color: 'bg-indigo-300', emoji: '📬', description: 'Private group messaging', domain: 'rinbox.online' }, - { id: 'choices', name: 'rChoices', badge: 'rCh', color: 'bg-fuchsia-300', emoji: '🔀', description: 'Collaborative decision making', domain: 'rchoices.online' }, + // Communicating + { id: 'chats', name: 'rChats', badge: 'rCh', color: 'bg-emerald-200', emoji: '💬', description: 'Real-time encrypted messaging', domain: 'rchats.online' }, + { id: 'inbox', name: 'rInbox', badge: 'rI', color: 'bg-indigo-300', emoji: '📬', description: 'Private group messaging', domain: 'rinbox.online' }, + { id: 'mail', name: 'rMail', badge: 'rMa', color: 'bg-blue-200', emoji: '✉️', description: 'Community email & newsletters', domain: 'rmail.online' }, + { id: 'forum', name: 'rForum', badge: 'rFo', color: 'bg-amber-200', emoji: '💭', description: 'Threaded community discussions', domain: 'rforum.online' }, + // Deciding + { id: 'choices', name: 'rChoices', badge: 'rCo', color: 'bg-fuchsia-300', emoji: '🔀', description: 'Collaborative decision making', domain: 'rchoices.online' }, { id: 'vote', name: 'rVote', badge: 'rV', color: 'bg-violet-300', emoji: '🗳️', description: 'Real-time polls & governance', domain: 'rvote.online' }, // Funding & Commerce { id: 'funds', name: 'rFunds', badge: 'rF', color: 'bg-lime-300', emoji: '💸', description: 'Collaborative fundraising & grants', domain: 'rfunds.online' }, - { id: 'wallet', name: 'rWallet', badge: 'rW', color: 'bg-yellow-300', emoji: '💰', description: 'Multi-chain crypto wallet', domain: 'rwallet.online' }, + { id: 'wallet', name: 'rWallet', badge: 'rW', color: 'bg-yellow-300', emoji: '💰', description: 'Multi-chain crypto wallet', domain: 'rwallet.online' }, { id: 'cart', name: 'rCart', badge: 'rCt', color: 'bg-orange-300', emoji: '🛒', description: 'Group commerce & shared shopping', domain: 'rcart.online' }, - { id: 'swag', name: 'rSwag', badge: 'rSw', color: 'bg-cyan-300', emoji: '👕', description: 'Community merch & print-on-demand', domain: 'rswag.online' }, - { id: 'auctions', name: 'rAuctions', badge: 'rA', color: 'bg-red-300', emoji: '🔨', description: 'Live auction platform', domain: 'rauctions.online' }, - // Social & Sharing - { id: 'network', name: 'rNetwork', badge: 'rNe', color: 'bg-blue-300', emoji: '🌐', description: 'Community network & social graph', domain: 'rnetwork.online' }, - { id: 'files', name: 'rFiles', badge: 'rFi', color: 'bg-cyan-300', emoji: '📁', description: 'Collaborative file storage', domain: 'rfiles.online' }, + { id: 'auctions', name: 'rAuctions', badge: 'rA', color: 'bg-red-300', emoji: '🔨', description: 'Live auction platform', domain: 'rauctions.online' }, + { id: 'swag', name: 'rSwag', badge: 'rSw', color: 'bg-red-200', emoji: '👕', description: 'Community merch & swag store', domain: 'rswag.online' }, + // Social & Media + { id: 'photos', name: 'rPhotos', badge: 'rPh', color: 'bg-pink-200', emoji: '📸', description: 'Shared community photo albums', domain: 'rphotos.online' }, { id: 'tube', name: 'rTube', badge: 'rTu', color: 'bg-pink-300', emoji: '🎬', description: 'Group video platform', domain: 'rtube.online' }, + { id: 'network', name: 'rNetwork', badge: 'rNe', color: 'bg-blue-300', emoji: '🌐', description: 'Community network & social graph', domain: 'rnetwork.online' }, + { id: 'socials', name: 'rSocials', badge: 'rSo', color: 'bg-sky-200', emoji: '📢', description: 'Social media management', domain: 'rsocials.online' }, + { id: 'files', name: 'rFiles', badge: 'rFi', color: 'bg-cyan-300', emoji: '📁', description: 'Collaborative file storage', domain: 'rfiles.online' }, { id: 'data', name: 'rData', badge: 'rD', color: 'bg-purple-300', emoji: '📊', description: 'Analytics & insights dashboard', domain: 'rdata.online' }, - { id: 'work', name: 'rWork', badge: 'rWk', color: 'bg-slate-300', emoji: '📋', description: 'Collective task management', domain: 'rwork.online' }, + // Work & Productivity + { id: 'work', name: 'rWork', badge: 'rWo', color: 'bg-slate-300', emoji: '💼', description: 'Project & task management', domain: 'rwork.online' }, + // Identity & Infrastructure + { id: 'ids', name: 'rIDs', badge: 'rId', color: 'bg-emerald-300', emoji: '🔑', description: 'Passkey identity & zero-knowledge auth', domain: 'ridentity.online' }, + { id: 'stack', name: 'rStack', badge: 'r*', color: 'bg-gradient-to-br from-cyan-300 via-violet-300 to-rose-300', emoji: '📦', description: 'Open-source community infrastructure', domain: 'rstack.online' }, ]; const MODULE_CATEGORIES: Record = { @@ -46,35 +56,56 @@ const MODULE_CATEGORIES: Record = { cal: 'Planning', trips: 'Planning', maps: 'Planning', - inbox: 'Discussing & Deciding', - choices: 'Discussing & Deciding', - vote: 'Discussing & Deciding', + chats: 'Communicating', + inbox: 'Communicating', + mail: 'Communicating', + forum: 'Communicating', + choices: 'Deciding', + vote: 'Deciding', funds: 'Funding & Commerce', wallet: 'Funding & Commerce', cart: 'Funding & Commerce', - swag: 'Funding & Commerce', auctions: 'Funding & Commerce', - network: 'Social & Sharing', - files: 'Social & Sharing', - tube: 'Social & Sharing', - data: 'Social & Sharing', - work: 'Social & Sharing', + swag: 'Funding & Commerce', + photos: 'Social & Media', + tube: 'Social & Media', + network: 'Social & Media', + socials: 'Social & Media', + files: 'Social & Media', + data: 'Social & Media', + work: 'Work & Productivity', + ids: 'Identity & Infrastructure', + stack: 'Identity & Infrastructure', }; const CATEGORY_ORDER = [ 'Creating', 'Planning', - 'Discussing & Deciding', + 'Communicating', + 'Deciding', 'Funding & Commerce', - 'Social & Sharing', + 'Social & Media', + 'Work & Productivity', + 'Identity & Infrastructure', ]; +/** Build the URL for a module, using username subdomain if logged in */ +function getModuleUrl(m: AppModule, username: string | null): string { + if (!m.domain) return '#'; + if (username) { + // Generate . URL + return `https://${username}.${m.domain}`; + } + return `https://${m.domain}`; +} + interface AppSwitcherProps { current?: string; } -export function AppSwitcher({ current = 'swag' }: AppSwitcherProps) { +export function AppSwitcher({ current = 'notes' }: AppSwitcherProps) { const [open, setOpen] = useState(false); + const [username, setUsername] = useState(null); const ref = useRef(null); useEffect(() => { @@ -87,8 +118,21 @@ export function AppSwitcher({ current = 'swag' }: AppSwitcherProps) { return () => document.removeEventListener('click', handleClick); }, []); + // Fetch current user's username for subdomain links + useEffect(() => { + fetch('/api/me') + .then((r) => r.json()) + .then((data) => { + if (data.authenticated && data.user?.username) { + setUsername(data.user.username); + } + }) + .catch(() => { /* not logged in */ }); + }, []); + const currentMod = MODULES.find((m) => m.id === current); + // Group modules by category const groups = new Map(); for (const m of MODULES) { const cat = MODULE_CATEGORIES[m.id] || 'Other'; @@ -98,6 +142,7 @@ export function AppSwitcher({ current = 'swag' }: AppSwitcherProps) { return (
+ {/* Trigger button */} + {/* Dropdown */} {open && (
+ {/* rStack header */}
r* @@ -123,6 +170,7 @@ export function AppSwitcher({ current = 'swag' }: AppSwitcherProps) {
+ {/* Categories */} {CATEGORY_ORDER.map((cat) => { const items = groups.get(cat); if (!items || items.length === 0) return null; @@ -139,13 +187,15 @@ export function AppSwitcher({ current = 'swag' }: AppSwitcherProps) { } transition-colors`} > setOpen(false)} > + {/* Pastel favicon badge */} {m.badge} + {/* Name + description */}
{m.name} @@ -172,6 +222,7 @@ export function AppSwitcher({ current = 'swag' }: AppSwitcherProps) { ); })} + {/* Footer */}
+
+ +

+ Part of the r* ecosystem — open source, self-hosted, community-owned +

+
+ + ); +} diff --git a/frontend/components/RevenueFlowSankey.tsx b/frontend/components/RevenueFlowSankey.tsx new file mode 100644 index 0000000..647244d --- /dev/null +++ b/frontend/components/RevenueFlowSankey.tsx @@ -0,0 +1,311 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface FlowSplits { + printer: number; + creator: number; + community: number; +} + +const DEFAULT_SALE_PRICE = 29.99; +const DEFAULT_PRODUCTION_COST = 9.25; + +// Min production cost as fraction of price (Printful cost floor) +const MIN_PRINTER_FRACTION = 0.15; + +export function RevenueFlowSankey() { + const [salePrice] = useState(DEFAULT_SALE_PRICE); + const [splits, setSplits] = useState(() => { + const printer = DEFAULT_PRODUCTION_COST / DEFAULT_SALE_PRICE; + const remaining = 1 - printer; + return { + printer, + creator: remaining * 0.35, + community: remaining * 0.65, + }; + }); + + const handleSplitChange = useCallback( + (key: keyof FlowSplits, newValue: number) => { + setSplits((prev) => { + const updated = { ...prev, [key]: newValue }; + + // Enforce minimum printer cost + if (updated.printer < MIN_PRINTER_FRACTION) { + updated.printer = MIN_PRINTER_FRACTION; + } + + // Normalize so all splits sum to 1 + const total = updated.printer + updated.creator + updated.community; + if (total === 0) return prev; + + return { + printer: updated.printer / total, + creator: updated.creator / total, + community: updated.community / total, + }; + }); + }, + [] + ); + + const printerAmount = salePrice * splits.printer; + const creatorAmount = salePrice * splits.creator; + const communityAmount = salePrice * splits.community; + + return ( +
+ {/* SVG Sankey Diagram */} +
+ + + + + + + + + + + + + + + {/* Glow filters */} + + + + + + + + + + {/* ── Source node: Sale ── */} + + + Sale + + + ${salePrice.toFixed(2)} + + + {/* ── Flow paths (Bezier curves) ── */} + + + + + {/* ── Target nodes ── */} + {/* Printer */} + + + P + + Printer + + + ${printerAmount.toFixed(2)} + + + ({(splits.printer * 100).toFixed(0)}%) + + + {/* Creator */} + + + C + + Creator + + + ${creatorAmount.toFixed(2)} + + + ({(splits.creator * 100).toFixed(0)}%) + + + {/* Community */} + + + $ + + Community + + + ${communityAmount.toFixed(2)} + + + ({(splits.community * 100).toFixed(0)}%) + + +
+ + {/* ── Interactive Sliders ── */} +
+ handleSplitChange("printer", v)} + /> + handleSplitChange("creator", v)} + /> + handleSplitChange("community", v)} + /> +
+ +

+ Drag the sliders to see how revenue flows between production, creator, and + community. The community sets its own margin — every dollar above production + cost funds collective work. +

+
+ ); +} + +/* ── Bezier flow path ── */ +function SankeyFlow({ + startX, + startY, + endX, + endY, + width, + gradient, +}: { + startX: number; + startY: number; + endX: number; + endY: number; + width: number; + gradient: string; +}) { + const midX = (startX + endX) / 2; + const d = `M ${startX} ${startY} C ${midX} ${startY}, ${midX} ${endY}, ${endX} ${endY}`; + + return ( + + ); +} + +/* ── Slider for a single flow channel ── */ +function FlowSlider({ + label, + sublabel, + value, + amount, + color, + onChange, +}: { + label: string; + sublabel: string; + value: number; + amount: number; + color: string; + onChange: (v: number) => void; +}) { + return ( +
+
+
+ + {label} + + + {sublabel} + +
+ + ${amount.toFixed(2)} + +
+ + onChange(Number(e.target.value) / 100)} + className="w-full h-2 rounded-full appearance-none cursor-pointer" + style={{ + background: `linear-gradient(to right, ${color} ${value * 100}%, hsl(var(--muted)) ${value * 100}%)`, + accentColor: color, + }} + aria-label={`${label} share: ${(value * 100).toFixed(0)}%`} + /> + +
+ {(value * 100).toFixed(0)}% +
+
+ ); +}