From 3cd8bbefa37ca6f030ec7dce3b8be394bea0c17a Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 14 Oct 2026 12:11:20 -0400 Subject: [PATCH] feat(website): grounded FAQ and validated contact on Support Center (W2) The Support page was a single static line plus a mailto link. There is no public ticket backend (only the internal ops console), so rather than simulate ticket submission, improve the page truthfully: - Add an FAQ grounded in features that actually ship (offline recording/sync, billing + 2FA managed in the mobile app, GPX/CSV export, session revocation, radar live-presence rules, public vs authenticated group rides). - Add a validated contact form (name/email/topic/message) that opens a prefilled email in the visitor's own client. It never claims a ticket was filed and states plainly that the message is not sent until the visitor sends it. - Keep support@motorsignal.app as the real, working fallback channel. - Preserve the existing public-page visual system (content-page / eyebrow / faq-section). Tests: FAQ + real mailto render; invalid input shows validation errors and no submission claim; valid input opens a mailto (no simulated ticket) with the topic and email encoded. Co-Authored-By: Claude Opus 4.8 --- website/app/support/page.tsx | 73 +++++++++- website/components/support-contact-form.tsx | 153 ++++++++++++++++++++ website/tests/support-contact.test.tsx | 74 ++++++++++ 3 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 website/components/support-contact-form.tsx create mode 100644 website/tests/support-contact.test.tsx diff --git a/website/app/support/page.tsx b/website/app/support/page.tsx index 4e09c33..7539bc3 100644 --- a/website/app/support/page.tsx +++ b/website/app/support/page.tsx @@ -1 +1,72 @@ -export default function SupportPage() { return
Support

Help for the road ahead.

Account and product support will be available here as the MotorSignal web platform comes online.

Contact support
; } +import { SupportContactForm } from '../../components/support-contact-form'; + +const SUPPORT_EMAIL = 'support@motorsignal.app'; + +// Grounded in features that actually ship today. Keep answers truthful: anything managed +// only in the mobile app, or not yet available on the web, is stated plainly. +const FAQ: { q: string; a: string }[] = [ + { + q: 'Can I use MotorSignal without an internet connection?', + a: 'Yes. Ride recording and telemetry run on-device, so you can record offline and while signed out. Your rides sync to the cloud once you are signed in and back online — nothing is lost in the meantime.', + }, + { + q: 'How do I manage my subscription or find my invoices?', + a: 'Subscriptions, payment methods, and invoices are managed in the MotorSignal mobile app. The web billing portal is not available yet, so the Account page will point you to the app for billing changes.', + }, + { + q: 'How do I turn on two-factor authentication (2FA)?', + a: 'Two-factor authentication is enrolled in the mobile app. The web Account page reflects your real 2FA status — it will show “Enabled” only once a factor is actually enrolled.', + }, + { + q: 'Can I export my ride data?', + a: 'Yes. On the web Account page, each ride can be exported as a GPX track or a CSV of its telemetry. Your historical exports are not altered after the fact.', + }, + { + q: 'How do I sign out other devices or browsers?', + a: 'Open Account → Active Device Sessions. You can revoke an individual session, or revoke all sessions at once. The session you are currently using is marked “this device”.', + }, + { + q: 'Why don’t I see live riders on the web radar?', + a: 'The Formation Radar shows live rider positions only for a group ride you are an accepted participant in. Outside of that it displays a clearly labelled demonstration, not real riders.', + }, + { + q: 'What’s the difference between the public Group Rides page and my hub?', + a: 'Signed out, the Group Rides page is a public showcase with sample formations. Signed in, it becomes your hub where you create, manage, join, and leave real rides.', + }, +]; + +export default function SupportPage() { + return ( +
+ Support +

Help for the road ahead.

+

+ Search the answers below for the quickest fix. If you still need a hand, send us an email and + we’ll get back to you. +

+ +
+

Contact support

+

+ Reach the team at{' '} + {SUPPORT_EMAIL}. Prefer a form? Fill this in and it will + open a prefilled email for you to send. +

+
+ +
+
+ +
+ Common questions +

Frequently asked

+ {FAQ.map((item) => ( +
+ {item.q} +

{item.a}

+
+ ))} +
+
+ ); +} diff --git a/website/components/support-contact-form.tsx b/website/components/support-contact-form.tsx new file mode 100644 index 0000000..9bc6c9f --- /dev/null +++ b/website/components/support-contact-form.tsx @@ -0,0 +1,153 @@ +'use client'; + +import React, { useState } from 'react'; + +const SUPPORT_EMAIL = 'support@motorsignal.app'; + +const CATEGORIES = [ + 'Account & sign-in', + 'Subscription & billing', + 'Group rides', + 'Clubs', + 'Telemetry & ride data', + 'Bug report', + 'Something else', +] as const; + +type Errors = { name?: string; email?: string; message?: string }; + +// Public support contact. There is no public ticket backend, so this never claims a +// ticket was filed: it validates input and opens the visitor's own email client with a +// prefilled message. Email remains the real, working support channel. +export function SupportContactForm() { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [category, setCategory] = useState<(typeof CATEGORIES)[number]>(CATEGORIES[0]); + const [message, setMessage] = useState(''); + const [errors, setErrors] = useState({}); + const [opened, setOpened] = useState(false); + + function validate(): Errors { + const next: Errors = {}; + if (!name.trim()) next.name = 'Please tell us your name.'; + // Pragmatic email check: a single @ with a dot in the domain. + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) { + next.email = 'Enter a valid email address so we can reply.'; + } + if (message.trim().length < 10) { + next.message = 'Add a few more details (at least 10 characters).'; + } + return next; + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + const found = validate(); + setErrors(found); + if (Object.keys(found).length > 0) { + setOpened(false); + return; + } + + const subject = `[${category}] Support request from ${name.trim()}`; + const body = + `Name: ${name.trim()}\n` + + `Email: ${email.trim()}\n` + + `Category: ${category}\n\n` + + `${message.trim()}\n`; + const mailto = `mailto:${SUPPORT_EMAIL}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + + // Open the visitor's email client. We do not and cannot confirm delivery here. + if (typeof window !== 'undefined') { + window.location.href = mailto; + } + setOpened(true); + } + + const inputClass = + 'w-full px-3.5 py-2.5 rounded-xl bg-surface-container-low border border-outline-variant/40 focus:border-primary-container focus:ring-1 focus:ring-primary-container text-white text-sm outline-none transition placeholder:text-slate-500'; + + return ( +
+
+
+ + setName(e.target.value)} + className={inputClass} + placeholder="Alex Rider" + aria-invalid={Boolean(errors.name)} + /> + {errors.name &&

{errors.name}

} +
+ +
+ + setEmail(e.target.value)} + className={inputClass} + placeholder="you@example.com" + aria-invalid={Boolean(errors.email)} + /> + {errors.email &&

{errors.email}

} +
+
+ +
+ + +
+ +
+ +