154 lines
5.4 KiB
TypeScript
154 lines
5.4 KiB
TypeScript
'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<Errors>({});
|
|
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 (
|
|
<form className="space-y-4 text-left" onSubmit={handleSubmit} noValidate>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="support-name" className="block text-xs font-mono tracking-wide text-on-surface-variant">
|
|
Your name
|
|
</label>
|
|
<input
|
|
id="support-name"
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
className={inputClass}
|
|
placeholder="Alex Rider"
|
|
aria-invalid={Boolean(errors.name)}
|
|
/>
|
|
{errors.name && <p className="form-error" role="alert">{errors.name}</p>}
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="support-email" className="block text-xs font-mono tracking-wide text-on-surface-variant">
|
|
Email
|
|
</label>
|
|
<input
|
|
id="support-email"
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className={inputClass}
|
|
placeholder="you@example.com"
|
|
aria-invalid={Boolean(errors.email)}
|
|
/>
|
|
{errors.email && <p className="form-error" role="alert">{errors.email}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="support-category" className="block text-xs font-mono tracking-wide text-on-surface-variant">
|
|
Topic
|
|
</label>
|
|
<select
|
|
id="support-category"
|
|
value={category}
|
|
onChange={(e) => setCategory(e.target.value as (typeof CATEGORIES)[number])}
|
|
className={inputClass}
|
|
>
|
|
{CATEGORIES.map((c) => (
|
|
<option key={c} value={c}>{c}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="support-message" className="block text-xs font-mono tracking-wide text-on-surface-variant">
|
|
How can we help?
|
|
</label>
|
|
<textarea
|
|
id="support-message"
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
rows={5}
|
|
className={inputClass}
|
|
placeholder="Describe what you ran into, including your device and app version if relevant."
|
|
aria-invalid={Boolean(errors.message)}
|
|
/>
|
|
{errors.message && <p className="form-error" role="alert">{errors.message}</p>}
|
|
</div>
|
|
|
|
<button type="submit" className="button">Compose support email</button>
|
|
|
|
{opened && (
|
|
<p className="form-message" role="status">
|
|
Your email app should have opened with the details filled in. If nothing happened, email{' '}
|
|
<a className="underline" href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a> directly.
|
|
</p>
|
|
)}
|
|
|
|
<p className="form-divider">
|
|
This opens your own email app — we don’t submit a ticket on your behalf, so your message
|
|
isn’t sent until you send it.
|
|
</p>
|
|
</form>
|
|
);
|
|
}
|