feat(storefront): update FAQ and legal documentation

- Added new FAQ sections for account security, ordering and checkout, returns, shipping, and contact information.
- Introduced legal documents including privacy policy, terms of service, data protection, and general terms and conditions.
- Updated package dependencies to include gray-matter and remark-gfm for enhanced markdown support.
This commit is contained in:
2026-03-13 21:39:25 +03:00
parent f1dbf0b6ee
commit c8f5d8d096
52 changed files with 2273 additions and 261 deletions

View File

@@ -0,0 +1,230 @@
"use client";
import { useState } from "react";
import { useMutation } from "convex/react";
import { api } from "../../../../../convex/_generated/api";
import type { Key } from "react-aria";
import {
Form,
TextField,
Label,
Input,
TextArea,
FieldError,
Button,
Spinner,
toast,
Select,
ListBox,
} from "@heroui/react";
const TOPIC_OPTIONS = [
{ key: "products", label: "Products" },
{ key: "orders", label: "Orders" },
{ key: "support", label: "Support" },
{ key: "other", label: "Other" },
] as const;
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const MAX_NAME = 200;
const MAX_EMAIL = 254;
const MAX_MESSAGE = 5000;
export function ContactForm() {
const submitMessage = useMutation(api.messages.submit);
const [isSubmitting, setIsSubmitting] = useState(false);
const [topicKey, setTopicKey] = useState<Key | null>(null);
const [topicError, setTopicError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setTopicError(null);
const form = e.currentTarget;
const formData = new FormData(form);
const fullName = (formData.get("fullName") as string)?.trim() ?? "";
const email = (formData.get("email") as string)?.trim() ?? "";
const message = (formData.get("message") as string)?.trim() ?? "";
// Client-side validation
if (!fullName) {
toast.danger("Please enter your full name.");
return;
}
if (fullName.length > MAX_NAME) {
toast.danger(`Full name must be at most ${MAX_NAME} characters.`);
return;
}
if (!email) {
toast.danger("Please enter your work email.");
return;
}
if (!EMAIL_REGEX.test(email)) {
toast.danger("Please enter a valid email address.");
return;
}
if (email.length > MAX_EMAIL) {
toast.danger("Email must be at most 254 characters.");
return;
}
const topic = topicKey as string | null;
if (!topic || !TOPIC_OPTIONS.some((o) => o.key === topic)) {
setTopicError("Please select a topic.");
toast.danger("Please select a topic.");
return;
}
if (!message) {
toast.danger("Please enter your message.");
return;
}
if (message.length > MAX_MESSAGE) {
toast.danger(`Message must be at most ${MAX_MESSAGE} characters.`);
return;
}
setIsSubmitting(true);
try {
await submitMessage({
fullName,
email,
topic: topic as "products" | "orders" | "support" | "other",
message,
});
toast.success("Thank you! We've received your message and will get back to you soon.");
form.reset();
setTopicKey(null);
setTopicError(null);
} catch (err: unknown) {
const messageErr = err instanceof Error ? err.message : "Something went wrong. Please try again.";
toast.danger(messageErr);
} finally {
setIsSubmitting(false);
}
};
return (
<Form onSubmit={handleSubmit} className="flex flex-col gap-5 w-full">
<TextField
isRequired
name="fullName"
maxLength={MAX_NAME}
className="flex flex-col gap-1"
aria-required="true"
>
<Label className="text-sm font-medium text-[var(--foreground)]">
Full name <span className="text-danger">*</span>
</Label>
<Input
placeholder="First and last name"
className="bg-[var(--surface)]"
autoComplete="name"
disabled={isSubmitting}
aria-required="true"
/>
<FieldError className="text-xs text-danger" />
</TextField>
<TextField
isRequired
name="email"
type="email"
maxLength={MAX_EMAIL}
validate={(val: string) => {
if (val && !EMAIL_REGEX.test(val)) return "Please enter a valid email address.";
}}
className="flex flex-col gap-1"
aria-required="true"
>
<Label className="text-sm font-medium text-[var(--foreground)]">
Work email address <span className="text-danger">*</span>
</Label>
<Input
type="email"
placeholder="me@company.com"
className="bg-[var(--surface)]"
autoComplete="email"
disabled={isSubmitting}
aria-required="true"
/>
<FieldError className="text-xs text-danger" />
</TextField>
<div className="flex flex-col gap-1">
<Label className="text-sm font-medium text-[var(--foreground)]">
Topic <span className="text-danger">*</span>
</Label>
<Select
aria-label="Select a topic"
aria-required="true"
placeholder="Select a topic"
value={topicKey}
onChange={(value) => {
setTopicKey(value ?? null);
setTopicError(null);
}}
isDisabled={isSubmitting}
className="w-full"
>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover className="rounded-lg">
<ListBox>
{TOPIC_OPTIONS.map((opt) => (
<ListBox.Item key={opt.key} id={opt.key} textValue={opt.label}>
{opt.label}
<ListBox.ItemIndicator />
</ListBox.Item>
))}
</ListBox>
</Select.Popover>
</Select>
{topicError && (
<p className="text-xs text-danger mt-1" role="alert">
{topicError}
</p>
)}
</div>
<TextField
isRequired
name="message"
maxLength={MAX_MESSAGE}
className="flex flex-col gap-1"
aria-required="true"
>
<Label className="text-sm font-medium text-[var(--foreground)]">
Your message <span className="text-danger">*</span>
</Label>
<TextArea
rows={5}
placeholder="Write your message"
className="bg-[var(--surface)]"
disabled={isSubmitting}
aria-required="true"
/>
<FieldError className="text-xs text-danger" />
</TextField>
<Button
type="submit"
isDisabled={isSubmitting}
isPending={isSubmitting}
className="bg-[#f4a13a] text-[#1a2e2d] font-medium w-full md:w-auto md:self-start mt-1"
aria-busy={isSubmitting}
>
{({ isPending }: { isPending: boolean }) =>
isPending ? (
<>
<Spinner color="current" size="sm" />
Submitting
</>
) : (
"Submit"
)
}
</Button>
</Form>
);
}