Add support page, update privacy policy with leaderboard disclosure

- New /support page with bug report, feature request, and feedback cards
- ContactForm accepts source prop ('inquiry' | 'support') for differentiated emails
- Support emails go to apps@such.software with 'App feedback' subject
- Added 'High Scores & Leaderboards' section to Bauhaus Echo privacy policy
- Added 'Support & Feedback' link to Bauhaus Echo product page footer
- Updated privacy policy date to February 18, 2026
This commit is contained in:
jwinterm
2026-02-18 17:01:55 -05:00
parent 6a3e609c41
commit 1e4af3da6d
5 changed files with 139 additions and 24 deletions
+18 -8
View File
@@ -9,8 +9,9 @@ const resend = new Resend(process.env.RESEND_API_KEY);
const ContactSchema = z.object({ const ContactSchema = z.object({
name: z.string().min(2, "Name is too short"), name: z.string().min(2, "Name is too short"),
email: z.string().email("Invalid email address"), email: z.string().email("Invalid email address"),
message: z.string().min(10, "Please tell us more about your project"), message: z.string().min(10, "Please tell us more"),
token: z.string().min(1, "Security check failed") token: z.string().min(1, "Security check failed"),
source: z.enum(["inquiry", "support"]).default("inquiry"),
}); });
export async function submitContactForm(prevState: any, formData: FormData) { export async function submitContactForm(prevState: any, formData: FormData) {
@@ -30,17 +31,23 @@ export async function submitContactForm(prevState: any, formData: FormData) {
// 3. Send email via Resend // 3. Send email via Resend
try { try {
const { name, email, message } = validated.data; const { name, email, message, source } = validated.data;
const contactEmail = process.env.CONTACT_EMAIL || 'sales@such.software'; const contactEmail = process.env.CONTACT_EMAIL || 'sales@such.software';
const isSupport = source === 'support';
const subject = isSupport
? `App feedback from ${name}`
: `New inquiry from ${name}`;
const heading = isSupport ? 'App Support / Feedback' : 'New Project Inquiry';
const { error } = await resend.emails.send({ const { error } = await resend.emails.send({
from: 'Such Software <noreply@such.software>', from: 'Such Software <noreply@such.software>',
to: contactEmail, to: isSupport ? 'apps@such.software' : contactEmail,
replyTo: email, replyTo: email,
subject: `New inquiry from ${name}`, subject,
text: `Name: ${name}\nEmail: ${email}\n\nMessage:\n${message}`, text: `Name: ${name}\nEmail: ${email}\n\nMessage:\n${message}`,
html: ` html: `
<h2>New Project Inquiry</h2> <h2>${heading}</h2>
<p><strong>Name:</strong> ${name}</p> <p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p> <p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
<h3>Message:</h3> <h3>Message:</h3>
@@ -53,9 +60,12 @@ export async function submitContactForm(prevState: any, formData: FormData) {
return { success: false, message: "Failed to send message. Please try again." }; return { success: false, message: "Failed to send message. Please try again." };
} }
return { success: true, message: "Request received. We will be in touch shortly." }; return { success: true, message: isSupport
? "Thanks for your feedback! We'll get back to you shortly."
: "Request received. We will be in touch shortly."
};
} catch (e) { } catch (e) {
console.error('Email error:', e); console.error('Email error:', e);
return { success: false, message: "System error. Please contact us via email." }; return { success: false, message: "System error. Please contact us via email." };
} }
} }
@@ -84,10 +84,14 @@ export default function BauhausEchoPage() {
</div> </div>
<div className="flex items-center gap-4 text-sm text-muted-foreground"> <div className="flex items-center gap-4 text-sm text-muted-foreground">
<Link href="/products/bauhaus-echo/privacy" className="hover:text-foreground"> <Link href="/products/bauhaus-echo/privacy" className="hover:text-foreground transition-colors">
Privacy Policy Privacy Policy
</Link> </Link>
<span>•</span> <span>•</span>
<Link href="/support" className="hover:text-foreground transition-colors">
Support &amp; Feedback
</Link>
<span>•</span>
<span>Contact: apps@such.software</span> <span>Contact: apps@such.software</span>
</div> </div>
</div> </div>
@@ -21,7 +21,7 @@ export default function BauhausEchoPrivacyPage() {
<div className="glass-card mt-6"> <div className="glass-card mt-6">
<header className="mb-8 pb-6 border-b border-border"> <header className="mb-8 pb-6 border-b border-border">
<h1 className="text-3xl md:text-4xl font-bold">Privacy Policy for Bauhaus Echo</h1> <h1 className="text-3xl md:text-4xl font-bold">Privacy Policy for Bauhaus Echo</h1>
<p className="text-muted-foreground mt-2">Last Updated: February 6, 2026</p> <p className="text-muted-foreground mt-2">Last Updated: February 18, 2026</p>
</header> </header>
<div className="prose dark:prose-invert max-w-none"> <div className="prose dark:prose-invert max-w-none">
@@ -89,6 +89,20 @@ export default function BauhausEchoPrivacyPage() {
We do not have access to your payment information. We only receive confirmation of your purchase status. We do not have access to your payment information. We only receive confirmation of your purchase status.
</p> </p>
<h3>High Scores &amp; Leaderboards</h3>
<p>
If you submit scores to our online leaderboards, we collect:
</p>
<ul>
<li>Your chosen display name (anonymous by default)</li>
<li>Your game scores and statistics</li>
<li>A randomly generated anonymous player ID</li>
</ul>
<p>
We do <strong>not</strong> require or collect your real name, email, or any other
personal information to use leaderboards.
</p>
<h3>Locally Stored Data</h3> <h3>Locally Stored Data</h3>
<p>The App stores the following data locally on your device:</p> <p>The App stores the following data locally on your device:</p>
<ul> <ul>
+67
View File
@@ -0,0 +1,67 @@
import { ContactForm } from "@/components/contact/contact-form";
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import Link from "next/link";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Support | Such Software",
description: "Get help with Bauhaus Echo or any Such Software product. Report bugs, request features, or send feedback.",
alternates: { canonical: "/support" },
};
export default function SupportPage() {
return (
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
<Header />
<div id="main-content" className="z-10 w-full max-w-6xl mx-auto py-20 px-4 pb-24 md:pb-20">
<Link href="/" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block transition-colors">
&larr; Home
</Link>
<h1 className="text-4xl md:text-5xl font-bold mb-4">Support &amp; Feedback</h1>
<p className="text-xl text-muted-foreground mb-12 max-w-2xl text-balance">
Having trouble with one of our apps? Want to suggest a feature? We&apos;d love to hear from you.
</p>
<div className="grid md:grid-cols-3 gap-8 mb-16">
<div className="section-container border-l-4 border-blue-500">
<h3 className="font-bold mb-2">Bug Reports</h3>
<p className="text-sm text-muted-foreground">
Something not working right? Let us know what happened, your device, and OS version — we&apos;ll investigate.
</p>
</div>
<div className="section-container border-l-4 border-emerald-500">
<h3 className="font-bold mb-2">Feature Requests</h3>
<p className="text-sm text-muted-foreground">
Have an idea to make the game better? We&apos;re always looking for ways to improve.
</p>
</div>
<div className="section-container border-l-4 border-amber-500">
<h3 className="font-bold mb-2">General Feedback</h3>
<p className="text-sm text-muted-foreground">
Enjoying the game? Hate something? All feedback is welcome and helps us prioritize.
</p>
</div>
</div>
<ContactForm source="support" />
<div className="mt-16 text-center text-sm text-muted-foreground space-y-2">
<p>
You can also reach us directly at{" "}
<a href="mailto:apps@such.software" className="text-primary hover:underline">
apps@such.software
</a>
</p>
<p>
<Link href="/products/bauhaus-echo/privacy" className="hover:text-foreground transition-colors">
Privacy Policy
</Link>
</p>
</div>
</div>
<MobileNav />
</main>
);
}
@@ -6,7 +6,7 @@ import { TurnstileWidget } from './turnstile-widget';
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Button } from '@repo/ui/components/button'; import { Button } from '@repo/ui/components/button';
function SubmitButton() { function SubmitButton({ label = "Send Message" }: { label?: string }) {
const { pending } = useFormStatus(); const { pending } = useFormStatus();
return ( return (
<Button <Button
@@ -15,16 +15,22 @@ function SubmitButton() {
size="lg" size="lg"
className="w-full md:w-64 rounded-xl text-lg font-semibold" className="w-full md:w-64 rounded-xl text-lg font-semibold"
> >
{pending ? "Sending..." : "Send Message"} {pending ? "Sending..." : label}
</Button> </Button>
); );
} }
export function ContactForm() { interface ContactFormProps {
source?: 'inquiry' | 'support';
}
export function ContactForm({ source = 'inquiry' }: ContactFormProps) {
const [state, formAction] = useActionState(submitContactForm, { success: false, message: '' }); const [state, formAction] = useActionState(submitContactForm, { success: false, message: '' });
const [token, setToken] = useState<string>(''); const [token, setToken] = useState<string>('');
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
const isSupport = source === 'support';
// Reset form on successful submission // Reset form on successful submission
useEffect(() => { useEffect(() => {
if (state.success && formRef.current) { if (state.success && formRef.current) {
@@ -43,19 +49,27 @@ export function ContactForm() {
<div className="absolute bottom-0 left-0 w-64 h-64 bg-blue-500/5 rounded-full blur-3xl -ml-32 -mb-32" /> <div className="absolute bottom-0 left-0 w-64 h-64 bg-blue-500/5 rounded-full blur-3xl -ml-32 -mb-32" />
<div className="text-left mb-10 relative z-10"> <div className="text-left mb-10 relative z-10">
<h3 className="text-3xl font-bold tracking-tight">Project Inquiry</h3> <h3 className="text-3xl font-bold tracking-tight">
{isSupport ? 'Send Feedback' : 'Project Inquiry'}
</h3>
<p className="text-muted-foreground text-lg mt-3 max-w-2xl text-balance"> <p className="text-muted-foreground text-lg mt-3 max-w-2xl text-balance">
Tell us about your technical challenges. From Medusa v2 migrations to real-time custom engines, we're ready to architect your solution. {isSupport
? 'Found a bug? Have a feature request? We read every message and typically respond within 24 hours.'
: 'Tell us about your technical challenges. From Medusa v2 migrations to real-time custom engines, we\'re ready to architect your solution.'}
</p> </p>
</div> </div>
<input type="hidden" name="source" value={source} />
<div className="grid md:grid-cols-2 gap-8 relative z-10"> <div className="grid md:grid-cols-2 gap-8 relative z-10">
<div className="space-y-3"> <div className="space-y-3">
<label htmlFor="name" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Full Name</label> <label htmlFor="name" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">
{isSupport ? 'Your Name' : 'Full Name'}
</label>
<input <input
name="name" name="name"
id="name" id="name"
placeholder="Jamie Doe" placeholder={isSupport ? "Your name" : "Jamie Doe"}
required required
aria-describedby={state?.errors?.name ? "name-error" : undefined} aria-describedby={state?.errors?.name ? "name-error" : undefined}
aria-invalid={state?.errors?.name ? "true" : undefined} aria-invalid={state?.errors?.name ? "true" : undefined}
@@ -65,12 +79,14 @@ export function ContactForm() {
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<label htmlFor="email" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Work Email</label> <label htmlFor="email" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">
{isSupport ? 'Email' : 'Work Email'}
</label>
<input <input
name="email" name="email"
id="email" id="email"
type="email" type="email"
placeholder="jamie@example.com" placeholder={isSupport ? "your@email.com" : "jamie@example.com"}
required required
aria-describedby={state?.errors?.email ? "email-error" : undefined} aria-describedby={state?.errors?.email ? "email-error" : undefined}
aria-invalid={state?.errors?.email ? "true" : undefined} aria-invalid={state?.errors?.email ? "true" : undefined}
@@ -81,11 +97,15 @@ export function ContactForm() {
</div> </div>
<div className="space-y-3 relative z-10"> <div className="space-y-3 relative z-10">
<label htmlFor="message" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Project Details</label> <label htmlFor="message" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">
{isSupport ? 'Your Message' : 'Project Details'}
</label>
<textarea <textarea
name="message" name="message"
id="message" id="message"
placeholder="What are you building? Mention any specific constraints, timelines, or technologies..." placeholder={isSupport
? "Describe the issue or suggestion. Include your device, OS version, and game version if reporting a bug..."
: "What are you building? Mention any specific constraints, timelines, or technologies..."}
required required
aria-describedby={state?.errors?.message ? "message-error" : undefined} aria-describedby={state?.errors?.message ? "message-error" : undefined}
aria-invalid={state?.errors?.message ? "true" : undefined} aria-invalid={state?.errors?.message ? "true" : undefined}
@@ -104,7 +124,7 @@ export function ContactForm() {
</p> </p>
</div> </div>
<div className="w-full md:w-auto"> <div className="w-full md:w-auto">
<SubmitButton /> <SubmitButton label={isSupport ? "Send Feedback" : "Send Message"} />
</div> </div>
</div> </div>
@@ -119,11 +139,11 @@ export function ContactForm() {
}`} }`}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-xl" aria-hidden="true">{state.success ? "✓" : "⚠"}</span> <span className="text-xl" aria-hidden="true">{state.success ? "\u2713" : "\u26A0"}</span>
<p className="font-medium">{state.message}</p> <p className="font-medium">{state.message}</p>
</div> </div>
</div> </div>
)} )}
</form> </form>
); );
} }