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}

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