feat(web): mobile-responsive /templates + drop pre-launch SiteBanner
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
co-authored by Claude Opus 4.7
parent f80bd8afbe
commit 00c6692c7a
3 changed files with 103 additions and 93 deletions
-2
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>
+56 -27
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>
); );
} }
-17
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>
);
}