feat(site): webshops rewrite + demo gallery + Bloomword/Meatspace/Mushroom Capital

- /products/webshops: lead with per-tenant theming; add the real platform
  features (Printful POD, non-custodial crypto to own wallet, Stripe Connect
  with item-margin-only fee framing, SIWE/wallet login, Telegram + email
  notifications, automated tax); fix the incorrect "Vercel/Railway" hosting
  line to fully-managed self-hosting; remove PayPal/Coinbase (not live).
- Live Demos gallery (new component) with real screenshots of the 5 themed
  demo storefronts.
- Add three projects to the portfolio: Bloomword (now live, bloomword.earth),
  Meatspace (in dev, meatspace.place), Mushroom Capital (coming soon,
  mushroom.capital) with an icon, a detail page (VideoGame + FAQ JSON-LD),
  and a sitemap entry each.
- Copy: avoid em dashes (colon/semicolon/comma) across the products area.

Verified: tsc clean, next build green (50/50 static pages).
This commit is contained in:
jwinterm
2026-07-06 08:28:39 -04:00
parent c15dd74a8d
commit cbc8906715
15 changed files with 724 additions and 53 deletions
@@ -0,0 +1,106 @@
import Image from "next/image";
import { Card } from "@repo/ui/components/card";
// Literal class strings per color so Tailwind's JIT keeps them (no dynamic interpolation).
const cardBorder: Record<string, string> = {
amber: "border-amber-500/20 hover:border-amber-500/50",
blue: "border-blue-500/20 hover:border-blue-500/50",
emerald: "border-emerald-500/20 hover:border-emerald-500/50",
violet: "border-violet-500/20 hover:border-violet-500/50",
cyan: "border-cyan-500/20 hover:border-cyan-500/50",
};
const titleColor: Record<string, string> = {
amber: "text-amber-700 dark:text-amber-400",
blue: "text-blue-700 dark:text-blue-400",
emerald: "text-emerald-700 dark:text-emerald-400",
violet: "text-violet-700 dark:text-violet-400",
cyan: "text-cyan-700 dark:text-cyan-400",
};
type Demo = {
name: string;
slug: string;
tagline: string;
color: keyof typeof titleColor;
href: string;
};
const demos: Demo[] = [
{
name: "Artisan",
slug: "artisan",
tagline: "Warm, handcrafted-goods storefront with editorial product pages.",
color: "amber",
href: "https://artisan-demo.such.software",
},
{
name: "Merch",
slug: "merch",
tagline: "Bold band / brand merch shop wired to print-on-demand fulfillment.",
color: "blue",
href: "https://merch-demo.such.software",
},
{
name: "Crypto",
slug: "crypto",
tagline: "Non-custodial crypto checkout: Bitcoin, Monero, Wownero, and Grin.",
color: "emerald",
href: "https://crypto-demo.such.software",
},
{
name: "Yoga",
slug: "yoga",
tagline: "Calm studio storefront for classes, passes, and lifestyle goods.",
color: "violet",
href: "https://yoga-demo.such.software",
},
{
name: "Coaching",
slug: "coaching",
tagline: "Service-led site for sessions, packages, and digital products.",
color: "cyan",
href: "https://coaching-demo.such.software",
},
];
function DemoCard({ demo }: { demo: Demo }) {
return (
<a
href={demo.href}
target="_blank"
rel="noopener noreferrer"
className="block group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background rounded-[2rem]"
aria-label={`Open the ${demo.name} demo storefront in a new tab`}
>
<Card className={`glass-card h-full !p-0 overflow-hidden cursor-pointer transition-colors ${cardBorder[demo.color]}`}>
{/* Real screenshot of the live themed storefront — captured at 1280×800,
cropped to the hero/nav (the most brand-distinctive region). */}
<div className="h-40 w-full overflow-hidden">
<Image
src={`/images/demos/${demo.slug}.png`}
alt={`${demo.name} demo storefront`}
width={1280}
height={800}
className="h-40 w-full object-cover object-top transition-transform duration-700 group-hover:scale-[1.03]"
/>
</div>
<div className="p-6">
<h3 className={`text-xl font-bold mb-2 ${titleColor[demo.color]}`}>{demo.name}</h3>
<p className="text-sm text-muted-foreground leading-relaxed mb-3">{demo.tagline}</p>
<span className="text-sm font-medium text-foreground group-hover:underline">
Visit live demo →
</span>
</div>
</Card>
</a>
);
}
export function DemosGallery() {
return (
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
{demos.map((demo) => (
<DemoCard key={demo.slug} demo={demo} />
))}
</div>
);
}