feat(web): mobile-responsive /templates + drop pre-launch SiteBanner
All checks were successful
Deploy to Production / deploy (push) Successful in 57s
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:
parent
f80bd8afbe
commit
00c6692c7a
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user