buildmymcpserver/apps/web/app/(marketing)/contact/page.tsx
Marco Sadjadi ef30baf52a
All checks were successful
Deploy to Production / deploy (push) Successful in 57s
feat: Swiss-compliant launch — Impressum/AGB/Contact, support panel, DSG exports, cookie banner
Legal (Swiss minimum, no individual named):
- Impressum page (UWG Art. 3 lit. s) — provider, contact via support panel,
  no email required, jurisdiction = Switzerland
- AGB page — subscription terms, payment, cancellation, suspension on payment
  fail, 14-day money-back, AI-processing-per-tier disclosure, Swiss law +
  Swiss venue, modeled after typical Schweizer SaaS terms
- Privacy: Stripe added as subprocessor with full data-flow disclosure

Support panel replaces email contact entirely:
- @bmm/db: support_status enum + support_tickets + support_messages tables,
  migration applied to prod DB
- @bmm/api: support routes (user create/list/view/reply, admin list/view/reply
  /set-status), public /v1/contact for logged-out visitors with per-IP rate
  limit of 3 submissions/day to prevent spam-flood
- Web: /settings/support (list + new), /settings/support/[id] (conversation),
  /admin/support, /admin/support/[id]
- Public /contact form with email collection for guest tickets

Data rights (DSG Art. 25 / GDPR Art. 15+20):
- /v1/account/export returns user-scoped JSON of profile, org, servers,
  builds, audit, support tickets and messages — excludes hashes, encrypted
  secrets, other-user data
- /settings/account: download button + deletion-via-ticket workflow

Production-readiness gaps closed:
- org.suspended now blocks /v1/servers POST and /v1/servers/preview (402);
  webhook flagged this state but enforcement was missing
- Cookie banner: minimal, essential-cookies-only disclosure (Swiss DSG +
  GDPR compliant without dark-pattern consent UI), mounts on both layouts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:12:06 +02:00

134 lines
4.4 KiB
TypeScript

'use client';
import { Input, Label, Textarea } from '@/components/input';
import { Button } from '@/components/ui/button';
import { apiFetch } from '@/lib/api';
import Link from 'next/link';
import { useState } from 'react';
export default function ContactPage() {
const [email, setEmail] = useState('');
const [subject, setSubject] = useState('');
const [body, setBody] = useState('');
const [state, setState] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
const [error, setError] = useState<string | null>(null);
async function submit(e: React.FormEvent) {
e.preventDefault();
setState('sending');
setError(null);
try {
await apiFetch('/v1/contact', {
method: 'POST',
body: JSON.stringify({ email, subject, body }),
});
setState('sent');
} catch (err) {
setState('error');
const detail = (err as { detail?: { detail?: string; error?: string } }).detail;
setError(detail?.detail ?? detail?.error ?? (err as Error).message);
}
}
if (state === 'sent') {
return (
<div className="mx-auto max-w-2xl px-6 py-16">
<div className="panel p-6 text-center">
<h1 className="text-[20px] font-semibold tracking-tight">Message received</h1>
<p className="mt-2 text-[13.5px] text-[--color-fg-muted]">
Thank you we got your message. We&apos;ll reply to{' '}
<span className="text-[--color-fg]">{email}</span> within one business day.
</p>
<p className="mt-4 text-[12px] text-[--color-fg-subtle]">
<Link href="/" className="hover:text-[--color-fg]">
Back to home
</Link>
</p>
</div>
</div>
);
}
return (
<div className="mx-auto max-w-2xl px-6 py-14">
<header className="mb-8">
<div className="text-[11px] uppercase tracking-[0.16em] text-[--color-fg-subtle]">
Contact
</div>
<h1 className="mt-2 text-[28px] font-semibold tracking-tight">Talk to us</h1>
<p className="mt-3 text-[14px] leading-relaxed text-[--color-fg-muted]">
We don&apos;t do public email every conversation runs through our internal support
panel so nothing gets lost. Already have an account?{' '}
<Link href="/settings/support" className="text-[--color-accent] hover:underline">
Open a ticket from inside
</Link>
.
</p>
</header>
<form onSubmit={submit} className="panel space-y-4 p-5">
<div className="space-y-1.5">
<Label htmlFor="contact-email">Your email</Label>
<Input
id="contact-email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@company.com"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="contact-subject">Subject</Label>
<Input
id="contact-subject"
required
minLength={3}
maxLength={200}
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Briefly — what's this about?"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="contact-body" hint={`${body.length} / 10000`}>
Message
</Label>
<Textarea
id="contact-body"
required
rows={7}
minLength={10}
maxLength={10_000}
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="Tell us what's going on. We answer within one business day."
/>
</div>
{error && <p className="text-[12.5px] text-[--color-danger]">{error}</p>}
<div className="flex items-center justify-between pt-1">
<p className="text-[11px] text-[--color-fg-subtle]">
Submitting creates a support ticket see{' '}
<Link href="/privacy" className="hover:text-[--color-fg]">
privacy
</Link>
.
</p>
<Button
variant="primary"
size="md"
type="submit"
disabled={state === 'sending' || !email || subject.length < 3 || body.length < 10}
>
{state === 'sending' ? 'Sending…' : 'Send'}
</Button>
</div>
</form>
</div>
);
}