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

View File

@ -7,6 +7,8 @@ import { apiFetch } from '@/lib/api';
import { cn } from '@/lib/cn'; import { cn } from '@/lib/cn';
import { Logo } from '@/components/logo'; import { Logo } from '@/components/logo';
import { Input } from '@/components/input'; import { Input } from '@/components/input';
import { MobileActionBar } from '@/components/mobile-action-bar';
import { UserMenu } from '@/components/user-menu';
interface Template { interface Template {
id: string; id: string;
@ -89,38 +91,46 @@ export default function TemplatesMarketplace() {
return ( return (
<div className="flex min-h-screen flex-col"> <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"> <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="mx-auto flex h-12 max-w-6xl items-center justify-between gap-2 px-4 sm:px-6">
<div className="flex items-center gap-3"> <div className="flex min-w-0 items-center gap-3">
<Logo /> <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> </div>
<nav className="flex items-center gap-2"> <nav className="flex items-center gap-1.5 sm:gap-2">
{loggedIn ? ( {loggedIn ? (
<> <>
{/* Dashboard link + "+ New server" pill hidden on mobile
the UserMenu (avatar) + the MobileActionBar below cover
both navigation paths there. */}
<Link <Link
href="/dashboard" 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 Dashboard
</Link> </Link>
<Link <Link
href="/servers/new" 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 + New server
</Link> </Link>
<UserMenu />
</> </>
) : ( ) : (
<> <>
<Link <Link
href="/" 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 Home
</Link> </Link>
<Link <Link
href="/login" 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 Start building
</Link> </Link>
@ -130,24 +140,45 @@ export default function TemplatesMarketplace() {
</div> </div>
</header> </header>
<main className="mx-auto w-full max-w-6xl flex-1 px-6 py-12"> <main
<header className="mb-8 max-w-2xl"> 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]"> <div className="text-[11px] uppercase tracking-[0.16em] text-[--color-fg-subtle]">
Marketplace Marketplace
</div> </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 MCP server templates
</h1> </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 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. own credentials, fully isolated. The template author never sees your data.
</p> </p>
</header> </header>
<div className="mb-6 flex flex-wrap items-center gap-3 border-b border-[--color-border] pb-4"> {/* 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="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 && ( {loggedIn && (
<> <>
<div className="flex gap-1 rounded-md border border-[--color-border] bg-[--color-bg-subtle] p-0.5"> <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) => ( {(['all', 'mine'] as Scope[]).map((s) => (
<button <button
key={s} key={s}
@ -160,17 +191,17 @@ export default function TemplatesMarketplace() {
: 'text-[--color-fg-muted] hover:text-[--color-fg]', : 'text-[--color-fg-muted] hover:text-[--color-fg]',
)} )}
> >
{s === 'all' ? 'All templates' : 'My templates'} {s === 'all' ? 'All' : 'Mine'}
</button> </button>
))} ))}
</div> </div>
<div className="h-4 w-px bg-[--color-border]" /> <div className="hidden h-4 w-px shrink-0 bg-[--color-border] sm:block" />
</> </>
)} )}
{scope === 'all' && ( {scope === 'all' && (
<> <>
<div className="flex gap-1"> <div className="flex shrink-0 gap-1">
{(['trending', 'top', 'newest'] as Sort[]).map((s) => ( {(['trending', 'top', 'newest'] as Sort[]).map((s) => (
<button <button
key={s} key={s}
@ -187,14 +218,14 @@ export default function TemplatesMarketplace() {
</button> </button>
))} ))}
</div> </div>
<div className="h-4 w-px bg-[--color-border]" /> <div className="hidden h-4 w-px shrink-0 bg-[--color-border] sm:block" />
</> </>
)} )}
<select <select
value={category} value={category}
onChange={(e) => setCategory(e.target.value)} 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" 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> <option value="">All categories</option>
{categories.map((c) => ( {categories.map((c) => (
@ -203,13 +234,7 @@ export default function TemplatesMarketplace() {
</option> </option>
))} ))}
</select> </select>
<div className="flex-1" /> </div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search…"
className="w-60"
/>
</div> </div>
{!visible && <p className="mono text-[12px] text-[--color-fg-muted]">Loading</p>} {!visible && <p className="mono text-[12px] text-[--color-fg-muted]">Loading</p>}
@ -245,10 +270,14 @@ export default function TemplatesMarketplace() {
</main> </main>
<footer className="border-t border-[--color-border] py-8"> <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. Every template is isolated: forking creates your own container with your own secrets.
</div> </div>
</footer> </footer>
{/* Mobile tab-bar only when signed in. Logged-out marketplace
browsing keeps the simple marketing chrome (login CTA in header). */}
{loggedIn && <MobileActionBar />}
</div> </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>
);
}