feat(web): mobile-responsive /templates + drop pre-launch SiteBanner
All checks were successful
Deploy to Production / deploy (push) Successful in 57s

Two related polish items:

1. Remove the global blue Preview banner from app/layout.tsx and delete
   the SiteBanner component. The component's own comment said "Remove
   once the service is open for production use" — Stripe live billing,
   OAuth, and per-runner TLS are all wired now, so the pre-launch notice
   is misleading.

2. Mobile-responsive treatment for the standalone /templates page (it
   lives outside (dashboard) layout, so it didn't inherit the new
   mobile chrome from the dashboard pass):
   - Top header tightened: "/templates" breadcrumb + Dashboard link +
     "+ New server" pill all hidden on mobile (the avatar UserMenu +
     bottom MobileActionBar cover those paths).
   - Logged-in users now get the same MobileActionBar tab-bar at the
     bottom (Market tab active), giving consistent app-shell across
     dashboard pages.
   - Filter row stacks vertically on mobile with search on top (thumb
     reach), then a horizontally-scrollable chip row for scope / sort /
     category so segmented controls don't squeeze below their min-width.
   - h1 scales 32px → 24px on mobile; padding tightened to px-4 py-8.
   - main gets pb-24 when logged in so cards clear the tab bar.

Logged-out marketplace browsing keeps the simpler marketing chrome
(Logo + "Start building" CTA) — no tab-bar, since visitors don't have
a dashboard to navigate into yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marco Sadjadi 2026-05-26 06:43:56 +02:00
parent f80bd8afbe
commit 00c6692c7a
3 changed files with 103 additions and 93 deletions

View File

@ -1,5 +1,4 @@
import { JsonLd } from '@/components/json-ld';
import { SiteBanner } from '@/components/site-banner';
import {
SEO_KEYWORDS,
SITE_DESCRIPTION,
@ -63,7 +62,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
>
<body>
<JsonLd data={siteJsonLd()} />
<SiteBanner />
{children}
</body>
</html>

View File

@ -7,6 +7,8 @@ import { apiFetch } from '@/lib/api';
import { cn } from '@/lib/cn';
import { Logo } from '@/components/logo';
import { Input } from '@/components/input';
import { MobileActionBar } from '@/components/mobile-action-bar';
import { UserMenu } from '@/components/user-menu';
interface Template {
id: string;
@ -89,38 +91,46 @@ export default function TemplatesMarketplace() {
return (
<div className="flex min-h-screen flex-col">
<header className="sticky top-0 z-50 border-b border-[--color-border] bg-[--color-bg]/85 backdrop-blur-md">
<div className="mx-auto flex h-12 max-w-6xl items-center justify-between px-6">
<div className="flex items-center gap-3">
<div className="mx-auto flex h-12 max-w-6xl items-center justify-between gap-2 px-4 sm:px-6">
<div className="flex min-w-0 items-center gap-3">
<Logo />
<span className="text-[12.5px] text-[--color-fg-subtle]">/ templates</span>
{/* "/ templates" subtitle is redundant on mobile h1 below
already names the page. Keep on desktop as breadcrumb. */}
<span className="hidden text-[12.5px] text-[--color-fg-subtle] sm:inline">
/ templates
</span>
</div>
<nav className="flex items-center gap-2">
<nav className="flex items-center gap-1.5 sm:gap-2">
{loggedIn ? (
<>
{/* Dashboard link + "+ New server" pill hidden on mobile
the UserMenu (avatar) + the MobileActionBar below cover
both navigation paths there. */}
<Link
href="/dashboard"
className="text-[12.5px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
className="hidden text-[12.5px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg] sm:inline"
>
Dashboard
</Link>
<Link
href="/servers/new"
className="rounded-md bg-[--color-accent] px-3 py-1.5 text-[12.5px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8]"
className="hidden h-7 items-center gap-1.5 rounded-md bg-[--color-accent] px-2.5 text-[12px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8] sm:inline-flex"
>
+ New server
</Link>
<UserMenu />
</>
) : (
<>
<Link
href="/"
className="text-[12.5px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
className="hidden text-[12.5px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg] sm:inline"
>
Home
</Link>
<Link
href="/login"
className="rounded-md bg-[--color-accent] px-3 py-1.5 text-[12.5px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8]"
className="rounded-md bg-[--color-accent] px-2.5 py-1 text-[12px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8] sm:px-3 sm:py-1.5 sm:text-[12.5px]"
>
Start building
</Link>
@ -130,86 +140,101 @@ export default function TemplatesMarketplace() {
</div>
</header>
<main className="mx-auto w-full max-w-6xl flex-1 px-6 py-12">
<header className="mb-8 max-w-2xl">
<main
className={cn(
'mx-auto w-full max-w-6xl flex-1 px-4 py-8 sm:px-6 sm:py-12',
// Bottom-bar clearance on mobile for logged-in users only — the
// MobileActionBar is fixed bottom and would overlap the last row
// of template cards otherwise.
loggedIn && 'pb-24 sm:pb-12',
)}
>
<header className="mb-6 max-w-2xl sm:mb-8">
<div className="text-[11px] uppercase tracking-[0.16em] text-[--color-fg-subtle]">
Marketplace
</div>
<h1 className="mt-2 text-[32px] font-semibold tracking-tight">
<h1 className="mt-2 text-[24px] font-semibold tracking-tight sm:text-[32px]">
MCP server templates
</h1>
<p className="mt-3 text-[14px] leading-relaxed text-[--color-fg-muted]">
<p className="mt-2 text-[13px] leading-relaxed text-[--color-fg-muted] sm:mt-3 sm:text-[14px]">
Pre-built MCP servers from the community. Fork in one click your own container, your
own credentials, fully isolated. The template author never sees your data.
</p>
</header>
<div className="mb-6 flex flex-wrap items-center gap-3 border-b border-[--color-border] pb-4">
{loggedIn && (
<>
<div className="flex gap-1 rounded-md border border-[--color-border] bg-[--color-bg-subtle] p-0.5">
{(['all', 'mine'] as Scope[]).map((s) => (
<button
key={s}
type="button"
onClick={() => setScope(s)}
className={cn(
'rounded-[4px] px-2.5 py-1 text-[12.5px] capitalize transition-colors',
s === scope
? 'bg-[--color-bg-elevated] text-[--color-fg]'
: 'text-[--color-fg-muted] hover:text-[--color-fg]',
)}
>
{s === 'all' ? 'All templates' : 'My templates'}
</button>
))}
</div>
<div className="h-4 w-px bg-[--color-border]" />
</>
)}
{scope === 'all' && (
<>
<div className="flex gap-1">
{(['trending', 'top', 'newest'] as Sort[]).map((s) => (
<button
key={s}
type="button"
onClick={() => setSort(s)}
className={cn(
'rounded-md px-2.5 py-1 text-[12.5px] capitalize transition-colors',
s === sort
? 'bg-[--color-bg-elevated] text-[--color-fg]'
: 'text-[--color-fg-muted] hover:text-[--color-fg]',
)}
>
{s}
</button>
))}
</div>
<div className="h-4 w-px bg-[--color-border]" />
</>
)}
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="h-7 rounded-md border border-[--color-border] bg-[--color-bg-subtle] px-2 text-[12.5px] focus:border-[--color-accent] focus:outline-none"
>
<option value="">All categories</option>
{categories.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
<div className="flex-1" />
{/* Filter row: stacks vertically on mobile (search on top for thumb
reach), inline on desktop. The order utility on the Input flips
it to the right side at sm: while filters wrap normally. */}
<div className="mb-6 flex flex-col gap-3 border-b border-[--color-border] pb-4 sm:flex-row sm:flex-wrap sm:items-center">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search…"
className="w-60"
className="order-first w-full sm:order-last sm:ml-auto sm:w-60"
/>
{/* Chips row horizontally scrollable on narrow mobile so the
segmented controls never get squeezed below their min-width. */}
<div className="-mx-1 flex items-center gap-2 overflow-x-auto px-1 sm:m-0 sm:flex-wrap sm:gap-3 sm:overflow-visible sm:p-0">
{loggedIn && (
<>
<div className="flex shrink-0 gap-1 rounded-md border border-[--color-border] bg-[--color-bg-subtle] p-0.5">
{(['all', 'mine'] as Scope[]).map((s) => (
<button
key={s}
type="button"
onClick={() => setScope(s)}
className={cn(
'rounded-[4px] px-2.5 py-1 text-[12.5px] capitalize transition-colors',
s === scope
? 'bg-[--color-bg-elevated] text-[--color-fg]'
: 'text-[--color-fg-muted] hover:text-[--color-fg]',
)}
>
{s === 'all' ? 'All' : 'Mine'}
</button>
))}
</div>
<div className="hidden h-4 w-px shrink-0 bg-[--color-border] sm:block" />
</>
)}
{scope === 'all' && (
<>
<div className="flex shrink-0 gap-1">
{(['trending', 'top', 'newest'] as Sort[]).map((s) => (
<button
key={s}
type="button"
onClick={() => setSort(s)}
className={cn(
'rounded-md px-2.5 py-1 text-[12.5px] capitalize transition-colors',
s === sort
? 'bg-[--color-bg-elevated] text-[--color-fg]'
: 'text-[--color-fg-muted] hover:text-[--color-fg]',
)}
>
{s}
</button>
))}
</div>
<div className="hidden h-4 w-px shrink-0 bg-[--color-border] sm:block" />
</>
)}
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="h-7 shrink-0 rounded-md border border-[--color-border] bg-[--color-bg-subtle] px-2 text-[12.5px] focus:border-[--color-accent] focus:outline-none"
>
<option value="">All categories</option>
{categories.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</div>
</div>
{!visible && <p className="mono text-[12px] text-[--color-fg-muted]">Loading</p>}
@ -245,10 +270,14 @@ export default function TemplatesMarketplace() {
</main>
<footer className="border-t border-[--color-border] py-8">
<div className="mx-auto max-w-6xl px-6 text-[12px] text-[--color-fg-subtle]">
<div className="mx-auto max-w-6xl px-4 text-[12px] text-[--color-fg-subtle] sm:px-6">
Every template is isolated: forking creates your own container with your own secrets.
</div>
</footer>
{/* Mobile tab-bar only when signed in. Logged-out marketplace
browsing keeps the simple marketing chrome (login CTA in header). */}
{loggedIn && <MobileActionBar />}
</div>
);
}

View File

@ -1,17 +0,0 @@
// Temporary pre-launch notice. Remove this component and its import in
// app/layout.tsx once the service is open for production use.
// Background is #4f46e5 (not the #6366f1 accent) so white text clears the
// WCAG AA 4.5:1 contrast ratio for this small text — #6366f1 fell just short.
export function SiteBanner() {
return (
<div
style={{ backgroundColor: '#4f46e5' }}
className="border-b border-[#4338ca] px-4 py-2 text-center text-[12.5px] font-medium leading-snug text-white"
>
<span className="font-semibold uppercase tracking-wide">Preview</span>
<span className="px-1.5">·</span>
BuildMyMCPServer is not yet open for production use sign-ups, server generation and billing
are still being finalised, and data may be reset.
</div>
);
}