Admin Panel
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-09-29 14:57:37 +07:00
parent 04fbddba13
commit 8c6dad2a31
94 changed files with 4864 additions and 1151 deletions
+237 -182
View File
@@ -1,11 +1,11 @@
import { Head } from '@inertiajs/react';
import { Form, Head } from '@inertiajs/react';
import { useState } from 'react';
import type { FormEvent } from 'react';
import { Button, ButtonLink } from '@/components/site/button';
import Card from '@/components/site/card';
import Eyebrow from '@/components/site/eyebrow';
import {
Field,
Honeypot,
SelectInput,
TextArea,
TextInput,
@@ -19,13 +19,16 @@ import SectionHeader from '@/components/site/section-header';
import StepCard from '@/components/site/step-card';
import { common, serviceOptions } from '@/content/common';
import { contactContent } from '@/content/contact';
import { company, officeAddress, offices } from '@/data/site';
import { officeAddress } from '@/data/site';
import { useContent } from '@/hooks/use-locale';
import { useSiteInfo } from '@/hooks/use-site-info';
import { cn } from '@/lib/utils';
import { store } from '@/routes/consultations';
type Content = typeof contactContent.id;
export default function Contact() {
const site = useSiteInfo();
const t = useContent(contactContent);
return (
@@ -186,19 +189,19 @@ export default function Contact() {
</p>
<div className="flex flex-wrap items-center justify-center gap-4 pt-2">
<ButtonLink
href={company.whatsapp.href}
href={site.whatsapp.href}
size="sm"
icon="chat"
>
{t.faq.whatsapp} ({company.whatsapp.label})
{t.faq.whatsapp} ({site.whatsapp.label})
</ButtonLink>
<ButtonLink
href={company.phone.href}
href={site.phone.href}
variant="secondary"
size="sm"
icon="call"
>
{t.faq.phone} ({company.phone.label})
{t.faq.phone} ({site.phone.label})
</ButtonLink>
</div>
</div>
@@ -217,6 +220,7 @@ function CardLabel({ children }: { children: string }) {
}
function OfficeCard({ t }: { t: Content }) {
const site = useSiteInfo();
return (
<Card padding="lg">
<div className="pb-4">
@@ -226,7 +230,7 @@ function OfficeCard({ t }: { t: Content }) {
</h3>
</div>
<div className="flex flex-col gap-3">
{offices.map((office) => (
{site.offices.map((office) => (
<div
key={office.name}
className="bg-surface-container flex items-start gap-3 rounded-lg p-3.5"
@@ -240,10 +244,10 @@ function OfficeCard({ t }: { t: Content }) {
{officeAddress(office)}
</span>
<a
href={office.phone.href}
href={office.phoneHref}
className="text-on-surface hover:text-primary font-mono font-semibold"
>
{t.offices.tel} {office.phone.label}
{t.offices.tel} {office.phone}
</a>
</div>
</div>
@@ -275,31 +279,32 @@ function OfficeCard({ t }: { t: Content }) {
}
function ChannelsCard({ t }: { t: Content }) {
const site = useSiteInfo();
const channels = [
{
icon: 'call',
label: t.channels.phone,
value: company.phone.label,
href: company.phone.href,
value: site.phone.label,
href: site.phone.href,
},
{
icon: 'chat',
label: t.channels.whatsapp,
value: company.whatsapp.label,
href: `${company.whatsapp.href}?text=${encodeURIComponent(t.channels.whatsappMessage)}`,
value: site.whatsapp.label,
href: `${site.whatsapp.href}?text=${encodeURIComponent(t.channels.whatsappMessage)}`,
badge: t.channels.whatsappBadge,
},
{
icon: 'mail',
label: t.channels.email,
value: company.email.busdev,
href: `mailto:${company.email.busdev}`,
value: site.email.busdev,
href: `mailto:${site.email.busdev}`,
},
{
icon: 'headset_mic',
label: t.channels.support,
value: company.email.support,
href: `mailto:${company.email.support}`,
value: site.email.support,
href: `mailto:${site.email.support}`,
badge: t.channels.supportBadge,
},
];
@@ -398,12 +403,6 @@ function ConsultationRequestForm({ t }: { t: Content }) {
const [submitted, setSubmitted] = useState(false);
const form = t.form;
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSubmitted(true);
event.currentTarget.reset();
};
return (
<Card padding="lg" className="shadow-xl lg:p-10">
<div className="border-outline-variant/30 border-b pb-6">
@@ -416,162 +415,216 @@ function ConsultationRequestForm({ t }: { t: Content }) {
</p>
</div>
<form className="flex flex-col gap-6 pt-6" onSubmit={handleSubmit}>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Field id="full-name" label={form.name} required>
<TextInput
id="full-name"
name="name"
placeholder={form.namePlaceholder}
required
/>
</Field>
<Field id="company-name" label={form.company} required>
<TextInput
id="company-name"
name="company"
placeholder={form.companyPlaceholder}
required
/>
</Field>
<Field id="job-title" label={form.jobTitle} required>
<TextInput
id="job-title"
name="job_title"
placeholder={form.jobTitlePlaceholder}
required
/>
</Field>
<Field id="work-email" label={form.email} required>
<TextInput
id="work-email"
name="email"
type="email"
placeholder={form.emailPlaceholder}
required
/>
</Field>
<Field id="phone-number" label={form.phone} required>
<TextInput
id="phone-number"
name="phone"
type="tel"
placeholder="+62 812 XXXX XXXX"
required
/>
</Field>
<Field id="industry-sector" label={form.industry} required>
<SelectInput
id="industry-sector"
name="industry"
placeholder={form.industryPlaceholder}
options={form.industries}
required
/>
</Field>
</div>
<Field id="service-focus" label={form.service} required>
<SelectInput
id="service-focus"
name="service"
placeholder={form.servicePlaceholder}
options={serviceOptions(shared)}
required
/>
</Field>
<fieldset className="flex flex-col gap-2">
<legend className="text-on-surface pb-2 font-mono text-xs font-medium">
{form.timeline}
</legend>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{form.timelines.map((timeline, index) => (
<label
key={timeline.value}
className={cn(
'border-outline-variant/60 bg-surface-container-lowest text-on-surface hover:bg-surface-container-high flex cursor-pointer flex-col rounded-lg border p-3 transition-colors',
'has-checked:border-primary-container has-checked:bg-primary-container has-checked:text-white',
'has-focus-visible:ring-primary has-focus-visible:ring-2',
)}
>
<input
type="radio"
name="timeline"
value={timeline.value}
defaultChecked={index === 0}
className="sr-only"
<Form
{...store.form()}
options={{ preserveScroll: true }}
resetOnSuccess
onSuccess={() => setSubmitted(true)}
className="relative flex flex-col gap-6 pt-6"
>
{({ processing, errors }) => (
<>
<input type="hidden" name="source" value="contact" />
<Honeypot />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Field id="full-name" label={form.name} required>
<TextInput
id="full-name"
name="name"
placeholder={form.namePlaceholder}
required
/>
<span className="text-xs font-bold">
{timeline.label}
</span>
<span className="text-sm opacity-80">
{timeline.note}
</span>
</label>
))}
</div>
</fieldset>
<Field id="project-details" label={form.details}>
<TextArea
id="project-details"
name="details"
rows={4}
placeholder={form.detailsPlaceholder}
/>
</Field>
<div className="bg-surface-container-lowest flex items-start gap-3 rounded-lg p-3.5">
<input
id="nda-consent"
name="nda"
type="checkbox"
required
className="mt-0.5 h-4 w-4 shrink-0"
/>
<label
htmlFor="nda-consent"
className="text-on-surface-variant cursor-pointer text-sm"
>
<RichText text={form.nda} />
</label>
</div>
<Button type="submit" size="block" trailingIcon="arrow_forward">
{form.submit}
</Button>
{submitted && (
<div
role="status"
className="border-primary/40 bg-surface-container-high text-on-surface flex items-center gap-3 rounded-lg border p-4"
>
<Icon
name="check_circle"
className="text-primary text-2xl"
/>
<div>
<span className="font-bold">
{form.successTitle}
</span>
<p className="text-on-surface-variant text-sm">
{form.successMessage}
</p>
</Field>
<Field
id="company-name"
label={form.company}
required
>
<TextInput
id="company-name"
name="company"
placeholder={form.companyPlaceholder}
required
/>
</Field>
<Field
id="job-title"
label={form.jobTitle}
required
>
<TextInput
id="job-title"
name="job_title"
placeholder={form.jobTitlePlaceholder}
required
/>
</Field>
<Field id="work-email" label={form.email} required>
<TextInput
id="work-email"
name="email"
type="email"
placeholder={form.emailPlaceholder}
required
/>
</Field>
<Field
id="phone-number"
label={form.phone}
required
>
<TextInput
id="phone-number"
name="phone"
type="tel"
placeholder="+62 812 XXXX XXXX"
required
/>
</Field>
<Field
id="industry-sector"
label={form.industry}
required
>
<SelectInput
id="industry-sector"
name="industry"
placeholder={form.industryPlaceholder}
options={form.industries.map(
({ label }) => ({
value: label,
label,
}),
)}
required
/>
</Field>
</div>
</div>
)}
<div className="text-on-surface-variant flex items-center justify-center gap-2 pt-2 text-center font-mono text-[11px]">
<Icon name="lock" className="text-primary text-sm" />
{form.privacy}
</div>
</form>
<Field id="service-focus" label={form.service} required>
<SelectInput
id="service-focus"
name="service"
placeholder={form.servicePlaceholder}
options={serviceOptions(shared)}
required
/>
</Field>
<fieldset className="flex flex-col gap-2">
<legend className="text-on-surface pb-2 font-mono text-xs font-medium">
{form.timeline}
</legend>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{form.timelines.map((timeline, index) => (
<label
key={timeline.value}
className={cn(
'border-outline-variant/60 bg-surface-container-lowest text-on-surface hover:bg-surface-container-high flex cursor-pointer flex-col rounded-lg border p-3 transition-colors',
'has-checked:border-primary-container has-checked:bg-primary-container has-checked:text-white',
'has-focus-visible:ring-primary has-focus-visible:ring-2',
)}
>
<input
type="radio"
name="timeline"
value={timeline.value}
defaultChecked={index === 0}
className="sr-only"
/>
<span className="text-xs font-bold">
{timeline.label}
</span>
<span className="text-sm opacity-80">
{timeline.note}
</span>
</label>
))}
</div>
</fieldset>
<Field id="project-details" label={form.details}>
<TextArea
id="project-details"
name="details"
rows={4}
placeholder={form.detailsPlaceholder}
/>
</Field>
<div className="bg-surface-container-lowest flex items-start gap-3 rounded-lg p-3.5">
<input
id="nda-consent"
name="nda_agreed"
value="1"
type="checkbox"
required
className="mt-0.5 h-4 w-4 shrink-0"
/>
<label
htmlFor="nda-consent"
className="text-on-surface-variant cursor-pointer text-sm"
>
<RichText text={form.nda} />
</label>
</div>
{Object.keys(errors).length > 0 && (
<ul
role="alert"
className="border-primary/40 text-primary flex flex-col gap-1 rounded-lg border p-3 text-sm"
>
{Object.values(errors).map((message) => (
<li key={message}>{message}</li>
))}
</ul>
)}
<Button
type="submit"
size="block"
trailingIcon="arrow_forward"
disabled={processing}
>
{form.submit}
</Button>
{submitted && (
<div
role="status"
className="border-primary/40 bg-surface-container-high text-on-surface flex items-center gap-3 rounded-lg border p-4"
>
<Icon
name="check_circle"
className="text-primary text-2xl"
/>
<div>
<span className="font-bold">
{form.successTitle}
</span>
<p className="text-on-surface-variant text-sm">
{form.successMessage}
</p>
</div>
</div>
)}
<div className="text-on-surface-variant flex items-center justify-center gap-2 pt-2 text-center font-mono text-[11px]">
<Icon
name="lock"
className="text-primary text-sm"
/>
{form.privacy}
</div>
</>
)}
</Form>
</Card>
);
}
function OfficeMap({ t }: { t: Content }) {
const site = useSiteInfo();
return (
<div className="bg-surface-container relative flex min-h-96 w-full flex-col justify-end overflow-hidden rounded-2xl p-4 shadow-xl sm:p-6 lg:min-h-[480px]">
<img
@@ -582,7 +635,7 @@ function OfficeMap({ t }: { t: Content }) {
/>
<div className="from-surface via-surface/40 absolute inset-0 bg-gradient-to-t to-transparent" />
<div className="relative mt-40 grid grid-cols-1 gap-4 md:grid-cols-2">
{offices.map((office) => (
{site.offices.map((office) => (
<div
key={office.name}
className="border-outline-variant/40 bg-surface-container/95 flex flex-col gap-3 rounded-xl border p-5 shadow-2xl backdrop-blur-md"
@@ -601,15 +654,17 @@ function OfficeMap({ t }: { t: Content }) {
<p className="text-on-surface-variant text-sm leading-relaxed">
{officeAddress(office)}
</p>
<ButtonLink
href={office.mapsUrl}
variant="secondary"
size="sm"
icon="directions"
className="self-start"
>
{t.location.directions}
</ButtonLink>
{office.maps_url && (
<ButtonLink
href={office.maps_url}
variant="secondary"
size="sm"
icon="directions"
className="self-start"
>
{t.location.directions}
</ButtonLink>
)}
</div>
))}
</div>