Spruce up contact and repair request forms, fix Docker build timeouts
Some checks failed
Build and Deploy / deploy (push) Has been cancelled
Some checks failed
Build and Deploy / deploy (push) Has been cancelled
- Contact page: two-column layout with rover image and email card sidebar - Repair request: hero section, "How it works" steps sidebar, return address field, warranty/charges checkbox - Reduce static generation concurrency to prevent Docker build timeouts - Bump staticPageGenerationTimeout to 300s Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,11 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
staticPageGenerationTimeout: 120,
|
staticPageGenerationTimeout: 300,
|
||||||
|
experimental: {
|
||||||
|
staticGenerationMaxConcurrency: 8,
|
||||||
|
staticGenerationMinPagesPerWorker: 25,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
import { Container } from "@/components/layout/Container";
|
import { Container } from "@/components/layout/Container";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { FormField } from "@/components/ui/FormField";
|
import { FormField } from "@/components/ui/FormField";
|
||||||
@@ -34,7 +35,7 @@ export default function ContactForm() {
|
|||||||
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
return (
|
return (
|
||||||
<section className="bg-white py-16 sm:py-24">
|
<section className="bg-muted py-16 sm:py-24">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="mx-auto max-w-2xl text-center">
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||||
@@ -53,113 +54,150 @@ export default function ContactForm() {
|
|||||||
"w-full rounded-lg border border-border bg-white px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-brand focus:outline-none focus:ring-2 focus:ring-brand/20";
|
"w-full rounded-lg border border-border bg-white px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-brand focus:outline-none focus:ring-2 focus:ring-brand/20";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-white py-16 sm:py-24">
|
<section className="bg-muted py-16 sm:py-24">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="mx-auto max-w-2xl">
|
<div className="mb-10">
|
||||||
<div className="mb-12">
|
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||||
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
Contact us.
|
||||||
Contact us.
|
</h1>
|
||||||
</h1>
|
<p className="mt-3 text-foreground-light">
|
||||||
<p className="mt-4 text-foreground-light">
|
Have a question or need a quote? Fill in the form and we'll get back to you.
|
||||||
Email:{" "}
|
</p>
|
||||||
<a
|
</div>
|
||||||
href="mailto:contact@micromelon.com.au"
|
|
||||||
className="text-brand-dark underline hover:text-foreground"
|
<div className="grid gap-12 md:grid-cols-2">
|
||||||
>
|
{/* Form */}
|
||||||
contact@micromelon.com.au
|
<div>
|
||||||
</a>
|
<div className="rounded-2xl border border-border bg-white p-6 shadow-sm sm:p-8">
|
||||||
</p>
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<FormField
|
||||||
|
label="Your Name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Your full name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
placeholder="you@example.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
label="Phone Number"
|
||||||
|
name="phone"
|
||||||
|
type="tel"
|
||||||
|
placeholder="Your phone number"
|
||||||
|
value={phone}
|
||||||
|
onChange={(e) => setPhone(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* School search combobox */}
|
||||||
|
<div ref={institutionRef} className="relative">
|
||||||
|
<label htmlFor="institution" className="mb-2 block text-sm font-medium text-foreground">
|
||||||
|
School / Organisation
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="institution"
|
||||||
|
value={institution}
|
||||||
|
onChange={(e) => {
|
||||||
|
setInstitution(e.target.value);
|
||||||
|
setInstitutionOpen(true);
|
||||||
|
}}
|
||||||
|
onFocus={() => setInstitutionOpen(true)}
|
||||||
|
placeholder="Search or type your school name"
|
||||||
|
className={inputClasses}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
{institutionOpen && institution.length > 0 && (() => {
|
||||||
|
const query = institution.toLowerCase();
|
||||||
|
const matches = KNOWN_SCHOOLS.filter((s) => s.toLowerCase().includes(query));
|
||||||
|
const exactMatch = KNOWN_SCHOOLS.some((s) => s.toLowerCase() === query);
|
||||||
|
if (matches.length === 0 && !exactMatch) return null;
|
||||||
|
if (matches.length === 1 && matches[0].toLowerCase() === query) return null;
|
||||||
|
return (
|
||||||
|
<ul className="absolute z-10 mt-1 max-h-48 w-full overflow-y-auto rounded-lg border border-border bg-white shadow-lg">
|
||||||
|
{matches.map((school) => (
|
||||||
|
<li key={school}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setInstitution(school);
|
||||||
|
setInstitutionOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted"
|
||||||
|
>
|
||||||
|
{school}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="Message"
|
||||||
|
name="message"
|
||||||
|
type="textarea"
|
||||||
|
required
|
||||||
|
placeholder="How can we help?"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="submit" variant="primary">
|
||||||
|
SEND
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
{/* Sidebar */}
|
||||||
<FormField
|
<div>
|
||||||
label="Your Name"
|
<div className="sticky top-24 space-y-6">
|
||||||
name="name"
|
<div className="overflow-hidden rounded-2xl">
|
||||||
type="text"
|
<Image
|
||||||
required
|
src="/images/products/rover-candid.png"
|
||||||
placeholder="Your full name"
|
alt="Micromelon Rover"
|
||||||
value={name}
|
width={600}
|
||||||
onChange={(e) => setName(e.target.value)}
|
height={400}
|
||||||
/>
|
className="w-full object-cover"
|
||||||
<FormField
|
/>
|
||||||
label="Email"
|
</div>
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
placeholder="you@example.com"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
label="Phone Number"
|
|
||||||
name="phone"
|
|
||||||
type="tel"
|
|
||||||
placeholder="Your phone number"
|
|
||||||
value={phone}
|
|
||||||
onChange={(e) => setPhone(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* School search combobox */}
|
<div className="rounded-2xl border border-border bg-white p-5 shadow-sm">
|
||||||
<div ref={institutionRef} className="relative">
|
<div className="flex items-start gap-3">
|
||||||
<label htmlFor="institution" className="mb-2 block text-sm font-medium text-foreground">
|
<div className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-brand/10">
|
||||||
School / Organisation
|
<svg className="h-5 w-5 text-brand" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||||
</label>
|
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
|
||||||
<input
|
</svg>
|
||||||
type="text"
|
</div>
|
||||||
id="institution"
|
<div>
|
||||||
value={institution}
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Email</p>
|
||||||
onChange={(e) => {
|
<a
|
||||||
setInstitution(e.target.value);
|
href="mailto:contact@micromelon.com.au"
|
||||||
setInstitutionOpen(true);
|
className="text-sm font-medium text-brand-dark hover:underline"
|
||||||
}}
|
>
|
||||||
onFocus={() => setInstitutionOpen(true)}
|
contact@micromelon.com.au
|
||||||
placeholder="Search or type your school name"
|
</a>
|
||||||
className={inputClasses}
|
</div>
|
||||||
autoComplete="off"
|
</div>
|
||||||
/>
|
</div>
|
||||||
{institutionOpen && institution.length > 0 && (() => {
|
|
||||||
const query = institution.toLowerCase();
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
const matches = KNOWN_SCHOOLS.filter((s) => s.toLowerCase().includes(query));
|
We typically respond within 1 business day.
|
||||||
const exactMatch = KNOWN_SCHOOLS.some((s) => s.toLowerCase() === query);
|
</p>
|
||||||
if (matches.length === 0 && !exactMatch) return null;
|
|
||||||
if (matches.length === 1 && matches[0].toLowerCase() === query) return null;
|
|
||||||
return (
|
|
||||||
<ul className="absolute z-10 mt-1 max-h-48 w-full overflow-y-auto rounded-lg border border-border bg-white shadow-lg">
|
|
||||||
{matches.map((school) => (
|
|
||||||
<li key={school}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setInstitution(school);
|
|
||||||
setInstitutionOpen(false);
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted"
|
|
||||||
>
|
|
||||||
{school}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<FormField
|
|
||||||
label="Message"
|
|
||||||
name="message"
|
|
||||||
type="textarea"
|
|
||||||
required
|
|
||||||
placeholder="How can we help?"
|
|
||||||
value={message}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button type="submit" variant="primary">
|
|
||||||
SEND
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
import { Container } from "@/components/layout/Container";
|
import { Container } from "@/components/layout/Container";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { SectionHeading } from "@/components/ui/SectionHeading";
|
|
||||||
import { KNOWN_SCHOOLS } from "@/data/schools";
|
import { KNOWN_SCHOOLS } from "@/data/schools";
|
||||||
|
|
||||||
interface RoverEntry {
|
interface RoverEntry {
|
||||||
@@ -23,6 +23,8 @@ export default function RepairRequestForm() {
|
|||||||
const [rovers, setRovers] = useState<RoverEntry[]>([
|
const [rovers, setRovers] = useState<RoverEntry[]>([
|
||||||
{ id: nextId++, serialNumber: "", issue: "" },
|
{ id: nextId++, serialNumber: "", issue: "" },
|
||||||
]);
|
]);
|
||||||
|
const [returnAddress, setReturnAddress] = useState("");
|
||||||
|
const [agreedToCharges, setAgreedToCharges] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
const institutionRef = useRef<HTMLDivElement>(null);
|
const institutionRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -63,6 +65,7 @@ export default function RepairRequestForm() {
|
|||||||
email,
|
email,
|
||||||
phone,
|
phone,
|
||||||
institution,
|
institution,
|
||||||
|
returnAddress,
|
||||||
rovers: rovers.map((r) => ({
|
rovers: rovers.map((r) => ({
|
||||||
serialNumber: r.serialNumber,
|
serialNumber: r.serialNumber,
|
||||||
issue: r.issue,
|
issue: r.issue,
|
||||||
@@ -79,7 +82,7 @@ export default function RepairRequestForm() {
|
|||||||
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
return (
|
return (
|
||||||
<section className="bg-white py-16 sm:py-24">
|
<section className="bg-muted py-16 sm:py-24">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="mx-auto max-w-2xl text-center">
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||||
@@ -97,183 +100,267 @@ export default function RepairRequestForm() {
|
|||||||
const inputClasses =
|
const inputClasses =
|
||||||
"w-full rounded-lg border border-border bg-white px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-brand focus:outline-none focus:ring-2 focus:ring-brand/20";
|
"w-full rounded-lg border border-border bg-white px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-brand focus:outline-none focus:ring-2 focus:ring-brand/20";
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ number: 1, title: "Submit your request", description: "Fill in the form with your rover details and the issues you're experiencing." },
|
||||||
|
{ number: 2, title: "Receive confirmation", description: "You'll receive a confirmation email acknowledging your repair request." },
|
||||||
|
{ number: 3, title: "Return Order PDF", description: "We'll email you a Return Order PDF with instructions on how and where to ship your rover." },
|
||||||
|
{ number: 4, title: "We repair your rovers", description: "Repairs normally take 1–2 weeks. If your rovers are out of warranty, you'll receive a quote before we proceed." },
|
||||||
|
{ number: 5, title: "Repaired & on its way", description: "Once repairs are complete, you'll receive an email letting you know your rover is on its way back." },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-white py-16 sm:py-24">
|
<>
|
||||||
<Container>
|
{/* Hero section */}
|
||||||
<div className="mx-auto max-w-2xl">
|
<section className="bg-muted">
|
||||||
<SectionHeading
|
<Container>
|
||||||
title="Rover Repair Request"
|
<div className="grid items-center gap-8 py-16 sm:py-20 md:grid-cols-2">
|
||||||
subtitle="Let us know which rovers need repair and we'll organise getting them fixed."
|
|
||||||
centered={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="name" className="mb-2 block text-sm font-medium text-foreground">
|
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||||
Name <span className="text-brand">*</span>
|
Rover Repair Request
|
||||||
</label>
|
</h1>
|
||||||
<input
|
<p className="mt-4 text-lg text-muted-foreground">
|
||||||
type="text"
|
Let us know which rovers need repair and we'll organise getting them fixed.
|
||||||
id="name"
|
</p>
|
||||||
required
|
</div>
|
||||||
placeholder="Your full name"
|
<div className="flex justify-center md:justify-end">
|
||||||
value={name}
|
<Image
|
||||||
onChange={(e) => setName(e.target.value)}
|
src="/images/products/rover-candid.png"
|
||||||
className={inputClasses}
|
alt="Micromelon Rover"
|
||||||
|
width={420}
|
||||||
|
height={280}
|
||||||
|
className="rounded-2xl object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Form + sidebar */}
|
||||||
|
<section className="bg-white py-16 sm:py-20">
|
||||||
|
<Container>
|
||||||
|
<div className="grid gap-12 md:grid-cols-2">
|
||||||
|
{/* Form */}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email" className="mb-2 block text-sm font-medium text-foreground">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
Email <span className="text-brand">*</span>
|
<div>
|
||||||
</label>
|
<label htmlFor="name" className="mb-2 block text-sm font-medium text-foreground">
|
||||||
<input
|
Name <span className="text-brand">*</span>
|
||||||
type="email"
|
</label>
|
||||||
id="email"
|
<input
|
||||||
required
|
type="text"
|
||||||
placeholder="you@example.com"
|
id="name"
|
||||||
value={email}
|
required
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
placeholder="Your full name"
|
||||||
className={inputClasses}
|
value={name}
|
||||||
/>
|
onChange={(e) => setName(e.target.value)}
|
||||||
</div>
|
className={inputClasses}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="phone" className="mb-2 block text-sm font-medium text-foreground">
|
<label htmlFor="email" className="mb-2 block text-sm font-medium text-foreground">
|
||||||
Phone
|
Email <span className="text-brand">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="email"
|
||||||
id="phone"
|
id="email"
|
||||||
placeholder="Your phone number"
|
required
|
||||||
value={phone}
|
placeholder="you@example.com"
|
||||||
onChange={(e) => setPhone(e.target.value)}
|
value={email}
|
||||||
className={inputClasses}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
/>
|
className={inputClasses}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* School search combobox */}
|
<div>
|
||||||
<div ref={institutionRef} className="relative">
|
<label htmlFor="phone" className="mb-2 block text-sm font-medium text-foreground">
|
||||||
<label htmlFor="institution" className="mb-2 block text-sm font-medium text-foreground">
|
Phone
|
||||||
School / Organisation <span className="text-brand">*</span>
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
type="tel"
|
||||||
type="text"
|
id="phone"
|
||||||
id="institution"
|
placeholder="Your phone number"
|
||||||
required
|
value={phone}
|
||||||
value={institution}
|
onChange={(e) => setPhone(e.target.value)}
|
||||||
onChange={(e) => {
|
className={inputClasses}
|
||||||
setInstitution(e.target.value);
|
/>
|
||||||
setInstitutionOpen(true);
|
</div>
|
||||||
}}
|
|
||||||
onFocus={() => setInstitutionOpen(true)}
|
{/* School search combobox */}
|
||||||
placeholder="Search or type your school name"
|
<div ref={institutionRef} className="relative">
|
||||||
className={inputClasses}
|
<label htmlFor="institution" className="mb-2 block text-sm font-medium text-foreground">
|
||||||
autoComplete="off"
|
School / Organisation <span className="text-brand">*</span>
|
||||||
/>
|
</label>
|
||||||
{institutionOpen && institution.length > 0 && (() => {
|
<input
|
||||||
const query = institution.toLowerCase();
|
type="text"
|
||||||
const matches = KNOWN_SCHOOLS.filter((s) => s.toLowerCase().includes(query));
|
id="institution"
|
||||||
const exactMatch = KNOWN_SCHOOLS.some((s) => s.toLowerCase() === query);
|
required
|
||||||
if (matches.length === 0 && !exactMatch) return null;
|
value={institution}
|
||||||
if (matches.length === 1 && matches[0].toLowerCase() === query) return null;
|
onChange={(e) => {
|
||||||
return (
|
setInstitution(e.target.value);
|
||||||
<ul className="absolute z-10 mt-1 max-h-48 w-full overflow-y-auto rounded-lg border border-border bg-white shadow-lg">
|
setInstitutionOpen(true);
|
||||||
{matches.map((school) => (
|
}}
|
||||||
<li key={school}>
|
onFocus={() => setInstitutionOpen(true)}
|
||||||
<button
|
placeholder="Search or type your school name"
|
||||||
type="button"
|
className={inputClasses}
|
||||||
onClick={() => {
|
autoComplete="off"
|
||||||
setInstitution(school);
|
/>
|
||||||
setInstitutionOpen(false);
|
{institutionOpen && institution.length > 0 && (() => {
|
||||||
}}
|
const query = institution.toLowerCase();
|
||||||
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted"
|
const matches = KNOWN_SCHOOLS.filter((s) => s.toLowerCase().includes(query));
|
||||||
>
|
const exactMatch = KNOWN_SCHOOLS.some((s) => s.toLowerCase() === query);
|
||||||
{school}
|
if (matches.length === 0 && !exactMatch) return null;
|
||||||
</button>
|
if (matches.length === 1 && matches[0].toLowerCase() === query) return null;
|
||||||
</li>
|
return (
|
||||||
|
<ul className="absolute z-10 mt-1 max-h-48 w-full overflow-y-auto rounded-lg border border-border bg-white shadow-lg">
|
||||||
|
{matches.map((school) => (
|
||||||
|
<li key={school}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setInstitution(school);
|
||||||
|
setInstitutionOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted"
|
||||||
|
>
|
||||||
|
{school}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rovers */}
|
||||||
|
<div>
|
||||||
|
<label className="mb-3 block text-sm font-medium text-foreground">
|
||||||
|
Rovers for Repair <span className="text-brand">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{rovers.map((rover, index) => (
|
||||||
|
<div
|
||||||
|
key={rover.id}
|
||||||
|
className="rounded-xl border border-border bg-muted/50 p-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
|
Rover {index + 1}
|
||||||
|
</span>
|
||||||
|
{rovers.length > 1 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeRover(rover.id)}
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-foreground">
|
||||||
|
Serial Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="00001234"
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="[0-9]{8}"
|
||||||
|
maxLength={8}
|
||||||
|
value={rover.serialNumber}
|
||||||
|
onChange={(e) => updateRover(rover.id, "serialNumber", e.target.value)}
|
||||||
|
className={inputClasses}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-foreground">
|
||||||
|
Issue <span className="text-brand">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
required
|
||||||
|
rows={2}
|
||||||
|
placeholder="Describe what's wrong with this rover"
|
||||||
|
value={rover.issue}
|
||||||
|
onChange={(e) => updateRover(rover.id, "issue", e.target.value)}
|
||||||
|
className={inputClasses}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rovers */}
|
|
||||||
<div>
|
|
||||||
<label className="mb-3 block text-sm font-medium text-foreground">
|
|
||||||
Rovers for Repair <span className="text-brand">*</span>
|
|
||||||
</label>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{rovers.map((rover, index) => (
|
|
||||||
<div
|
|
||||||
key={rover.id}
|
|
||||||
className="rounded-xl border border-border bg-muted/50 p-4"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
||||||
Rover {index + 1}
|
|
||||||
</span>
|
|
||||||
{rovers.length > 1 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeRover(rover.id)}
|
|
||||||
className="text-xs text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<label className="mb-1 block text-xs font-medium text-foreground">
|
|
||||||
Serial Number
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="00001234"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]{8}"
|
|
||||||
maxLength={8}
|
|
||||||
value={rover.serialNumber}
|
|
||||||
onChange={(e) => updateRover(rover.id, "serialNumber", e.target.value)}
|
|
||||||
className={inputClasses}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="mb-1 block text-xs font-medium text-foreground">
|
|
||||||
Issue <span className="text-brand">*</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
required
|
|
||||||
rows={2}
|
|
||||||
placeholder="Describe what's wrong with this rover"
|
|
||||||
value={rover.issue}
|
|
||||||
onChange={(e) => updateRover(rover.id, "issue", e.target.value)}
|
|
||||||
className={inputClasses}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<button
|
||||||
</div>
|
type="button"
|
||||||
<button
|
onClick={addRover}
|
||||||
type="button"
|
className="mt-3 flex items-center gap-2 rounded-lg border border-dashed border-border px-4 py-2.5 text-sm font-medium text-foreground-light hover:border-foreground-light hover:text-foreground transition-colors"
|
||||||
onClick={addRover}
|
>
|
||||||
className="mt-3 flex items-center gap-2 rounded-lg border border-dashed border-border px-4 py-2.5 text-sm font-medium text-foreground-light hover:border-foreground-light hover:text-foreground transition-colors"
|
<span className="text-lg leading-none">+</span>
|
||||||
>
|
Add another rover
|
||||||
<span className="text-lg leading-none">+</span>
|
</button>
|
||||||
Add another rover
|
</div>
|
||||||
</button>
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="returnAddress" className="mb-2 block text-sm font-medium text-foreground">
|
||||||
|
Return Address <span className="text-brand">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="returnAddress"
|
||||||
|
required
|
||||||
|
rows={3}
|
||||||
|
placeholder="Street address, suburb, state, postcode"
|
||||||
|
value={returnAddress}
|
||||||
|
onChange={(e) => setReturnAddress(e.target.value)}
|
||||||
|
className={inputClasses}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="agree-charges"
|
||||||
|
required
|
||||||
|
checked={agreedToCharges}
|
||||||
|
onChange={(e) => setAgreedToCharges(e.target.checked)}
|
||||||
|
className="mt-1 h-4 w-4 shrink-0 rounded border-border text-brand accent-brand focus:ring-brand/20"
|
||||||
|
/>
|
||||||
|
<label htmlFor="agree-charges" className="text-sm text-muted-foreground">
|
||||||
|
I understand that rover repairs within the standard 1-year warranty period are covered at no cost for manufacturing defects. Repairs outside the warranty period, or for damage caused by misuse, intentional damage, or neglect, are subject to a $50 service fee plus the cost of replacement parts. Micromelon reserves the right to assess the rover and provide a quote before proceeding with any chargeable repairs. <span className="text-brand">*</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="submit" variant="primary" disabled={submitting || !agreedToCharges}>
|
||||||
|
{submitting ? "Submitting..." : "Submit Repair Request"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
<div>
|
<div>
|
||||||
<Button type="submit" variant="primary" disabled={submitting}>
|
<div className="sticky top-24">
|
||||||
{submitting ? "Submitting..." : "Submit Repair Request"}
|
<div className="rounded-2xl border border-border bg-muted p-6">
|
||||||
</Button>
|
<h3 className="text-lg font-bold text-foreground">How it works</h3>
|
||||||
|
<div className="mt-5 space-y-8">
|
||||||
|
{steps.map((step) => (
|
||||||
|
<div key={step.number} className="flex gap-4">
|
||||||
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-brand text-sm font-bold text-white">
|
||||||
|
{step.number}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-foreground">{step.title}</p>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">{step.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
</Container>
|
</section>
|
||||||
</section>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user