619 lines
27 KiB
TypeScript
619 lines
27 KiB
TypeScript
import { 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,
|
|
SelectInput,
|
|
TextArea,
|
|
TextInput,
|
|
} from '@/components/site/form-field';
|
|
import Icon from '@/components/site/icon';
|
|
import IconTile from '@/components/site/icon-tile';
|
|
import PageHero from '@/components/site/page-hero';
|
|
import RichText from '@/components/site/rich-text';
|
|
import Section from '@/components/site/section';
|
|
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 { useContent } from '@/hooks/use-locale';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
type Content = typeof contactContent.id;
|
|
|
|
export default function Contact() {
|
|
const t = useContent(contactContent);
|
|
|
|
return (
|
|
<>
|
|
<Head title={t.title} />
|
|
|
|
<PageHero
|
|
badge={t.hero.badge}
|
|
title={t.hero.title}
|
|
description={<RichText text={t.hero.description} />}
|
|
>
|
|
<div className="flex flex-col gap-6">
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:max-w-3xl">
|
|
{t.hero.assurances.map((item) => (
|
|
<Card
|
|
key={item.icon}
|
|
padding="sm"
|
|
className="flex items-center gap-3"
|
|
>
|
|
<IconTile icon={item.icon} tone="solid" />
|
|
<div className="flex flex-col">
|
|
<span className="text-on-surface-variant font-mono text-[11px] font-semibold uppercase">
|
|
{item.label}
|
|
</span>
|
|
<span className="font-display text-on-surface text-lg font-semibold">
|
|
{item.value}
|
|
</span>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
<Card className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
{t.hero.highlights.map((item) => (
|
|
<div
|
|
key={item.icon}
|
|
className="flex items-start gap-3"
|
|
>
|
|
<Icon
|
|
name={item.icon}
|
|
className="text-primary mt-0.5 text-2xl"
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="text-on-surface-variant font-mono text-[11px] font-semibold uppercase">
|
|
{item.label}
|
|
</span>
|
|
<span className="text-on-surface text-sm font-semibold">
|
|
{item.value}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</Card>
|
|
</div>
|
|
</PageHero>
|
|
|
|
<Section>
|
|
<div className="grid grid-cols-1 items-start gap-8 lg:grid-cols-12">
|
|
<div className="flex flex-col gap-6 lg:col-span-5">
|
|
<OfficeCard t={t} />
|
|
<ChannelsCard t={t} />
|
|
<IncidentLineCard t={t} />
|
|
</div>
|
|
<div id="audit-form" className="lg:col-span-7">
|
|
<ConsultationRequestForm t={t} />
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
<Section id="lokasi" tone="muted">
|
|
<div className="flex flex-col gap-10">
|
|
<SectionHeader
|
|
align="split"
|
|
eyebrow={t.location.eyebrow}
|
|
title={t.location.title}
|
|
description={t.location.description}
|
|
/>
|
|
<OfficeMap t={t} />
|
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
|
{t.location.access.map((point) => (
|
|
<Card
|
|
key={point.icon}
|
|
className="flex items-start gap-4"
|
|
>
|
|
<IconTile
|
|
icon={point.icon}
|
|
size="md"
|
|
tone="neutral"
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-display text-on-surface text-lg font-bold">
|
|
{point.title}
|
|
</span>
|
|
<p className="text-on-surface-variant pt-1 text-sm leading-relaxed">
|
|
{point.description}
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
<Section>
|
|
<div className="flex flex-col gap-12">
|
|
<SectionHeader
|
|
align="center"
|
|
eyebrow={t.steps.eyebrow}
|
|
title={t.steps.title}
|
|
description={t.steps.description}
|
|
/>
|
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
|
{t.steps.items.map((step, index) => (
|
|
<StepCard
|
|
key={step.footer.icon}
|
|
step={String(index + 1).padStart(2, '0')}
|
|
{...step}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
<Section tone="muted" size="narrow">
|
|
<div className="flex flex-col gap-12">
|
|
<SectionHeader
|
|
align="center"
|
|
eyebrow={t.faq.eyebrow}
|
|
title={t.faq.title}
|
|
description={t.faq.description}
|
|
/>
|
|
<div className="space-y-4">
|
|
{t.faq.items.map((faq) => (
|
|
<Card
|
|
key={faq.question}
|
|
className="flex items-start gap-4"
|
|
>
|
|
<Icon
|
|
name="help"
|
|
className="text-primary mt-0.5 text-2xl"
|
|
/>
|
|
<div className="flex flex-col gap-2">
|
|
<h3 className="font-display text-on-surface text-lg font-bold">
|
|
{faq.question}
|
|
</h3>
|
|
<p className="text-on-surface-variant text-base leading-relaxed">
|
|
{faq.answer}
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
<div className="bg-surface-container-high flex flex-col items-center gap-4 rounded-2xl p-8 text-center">
|
|
<h3 className="font-display text-on-surface text-xl font-bold">
|
|
{t.faq.urgentTitle}
|
|
</h3>
|
|
<p className="text-on-surface-variant max-w-xl text-base">
|
|
{t.faq.urgentText}
|
|
</p>
|
|
<div className="flex flex-wrap items-center justify-center gap-4 pt-2">
|
|
<ButtonLink
|
|
href={company.whatsapp.href}
|
|
size="sm"
|
|
icon="chat"
|
|
>
|
|
{t.faq.whatsapp} ({company.whatsapp.label})
|
|
</ButtonLink>
|
|
<ButtonLink
|
|
href={company.phone.href}
|
|
variant="secondary"
|
|
size="sm"
|
|
icon="call"
|
|
>
|
|
{t.faq.phone} ({company.phone.label})
|
|
</ButtonLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function CardLabel({ children }: { children: string }) {
|
|
return (
|
|
<span className="text-on-surface-variant font-mono text-[11px] font-semibold uppercase">
|
|
{children}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function OfficeCard({ t }: { t: Content }) {
|
|
return (
|
|
<Card padding="lg">
|
|
<div className="pb-4">
|
|
<CardLabel>{t.offices.label}</CardLabel>
|
|
<h3 className="font-display text-on-surface text-lg font-bold">
|
|
{t.offices.title}
|
|
</h3>
|
|
</div>
|
|
<div className="flex flex-col gap-3">
|
|
{offices.map((office) => (
|
|
<div
|
|
key={office.name}
|
|
className="bg-surface-container flex items-start gap-3 rounded-lg p-3.5"
|
|
>
|
|
<IconTile icon={office.icon} tone="neutral" />
|
|
<div className="flex flex-col gap-1 text-sm">
|
|
<span className="text-on-surface font-bold">
|
|
{office.name}
|
|
</span>
|
|
<span className="text-on-surface-variant leading-relaxed">
|
|
{officeAddress(office)}
|
|
</span>
|
|
<a
|
|
href={office.phone.href}
|
|
className="text-on-surface hover:text-primary font-mono font-semibold"
|
|
>
|
|
{t.offices.tel} {office.phone.label}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="border-outline-variant/30 mt-4 grid grid-cols-2 gap-4 border-t pt-4 text-sm">
|
|
<div className="flex flex-col">
|
|
<CardLabel>{t.offices.hoursLabel}</CardLabel>
|
|
<span className="text-on-surface font-semibold">
|
|
{t.offices.hoursDays}
|
|
</span>
|
|
<span className="text-on-surface-variant">
|
|
{t.offices.hoursTime}
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<CardLabel>{t.offices.visitLabel}</CardLabel>
|
|
<span className="text-highlight inline-flex items-center gap-1.5 font-semibold">
|
|
<span className="bg-primary h-2 w-2 rounded-full" />
|
|
{t.offices.visitStatus}
|
|
</span>
|
|
<span className="text-on-surface-variant">
|
|
{t.offices.visitNote}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function ChannelsCard({ t }: { t: Content }) {
|
|
const channels = [
|
|
{
|
|
icon: 'call',
|
|
label: t.channels.phone,
|
|
value: company.phone.label,
|
|
href: company.phone.href,
|
|
},
|
|
{
|
|
icon: 'chat',
|
|
label: t.channels.whatsapp,
|
|
value: company.whatsapp.label,
|
|
href: `${company.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}`,
|
|
},
|
|
{
|
|
icon: 'headset_mic',
|
|
label: t.channels.support,
|
|
value: company.email.support,
|
|
href: `mailto:${company.email.support}`,
|
|
badge: t.channels.supportBadge,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Card padding="lg" className="flex flex-col gap-5">
|
|
<div>
|
|
<CardLabel>{t.channels.label}</CardLabel>
|
|
<h3 className="font-display text-on-surface text-lg font-bold">
|
|
{t.channels.title}
|
|
</h3>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{channels.map((channel) => {
|
|
const external = channel.href.startsWith('http');
|
|
|
|
return (
|
|
<a
|
|
key={channel.icon}
|
|
href={channel.href}
|
|
target={external ? '_blank' : undefined}
|
|
rel={external ? 'noopener noreferrer' : undefined}
|
|
className="group bg-surface-container hover:bg-surface-container-high flex items-center justify-between gap-3 rounded-lg p-3.5 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Icon
|
|
name={channel.icon}
|
|
className="text-primary text-2xl transition-transform group-hover:scale-110"
|
|
/>
|
|
<div className="flex flex-col">
|
|
<CardLabel>{channel.label}</CardLabel>
|
|
<span className="text-on-surface font-semibold">
|
|
{channel.value}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{channel.badge ? (
|
|
<span className="bg-surface-container-high text-on-surface group-hover:bg-primary-container shrink-0 rounded px-2 py-0.5 font-mono text-[11px] transition-colors group-hover:text-white">
|
|
{channel.badge}
|
|
</span>
|
|
) : (
|
|
<Icon
|
|
name="arrow_outward"
|
|
className="text-on-surface-variant group-hover:text-on-surface text-lg transition-colors"
|
|
/>
|
|
)}
|
|
</a>
|
|
);
|
|
})}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function IncidentLineCard({ t }: { t: Content }) {
|
|
return (
|
|
<Card
|
|
padding="lg"
|
|
className="from-primary-container/15 via-surface-container-low to-surface-container-low dark:from-primary-container/40 overflow-hidden bg-gradient-to-br"
|
|
>
|
|
<div className="flex items-center gap-3 pb-3">
|
|
<span className="bg-primary h-3 w-3 animate-ping rounded-full" />
|
|
<span className="text-primary font-mono text-[11px] font-bold tracking-wide">
|
|
{t.incident.badge}
|
|
</span>
|
|
</div>
|
|
<h3 className="font-display text-on-surface mb-2 text-lg font-bold">
|
|
{t.incident.title}
|
|
</h3>
|
|
<p className="text-on-surface-variant mb-4 text-sm leading-relaxed">
|
|
<RichText text={t.incident.description} />
|
|
</p>
|
|
<div className="bg-surface-container-lowest flex items-center justify-between gap-3 rounded-lg p-3">
|
|
<div className="flex items-center gap-2">
|
|
<Icon name="timer" className="text-primary text-lg" />
|
|
<span className="text-on-surface font-mono text-[11px] font-semibold">
|
|
{t.incident.slaLabel}
|
|
</span>
|
|
</div>
|
|
<span className="font-display text-primary text-lg font-bold">
|
|
{t.incident.slaValue}
|
|
</span>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Full consultation request form.
|
|
*
|
|
* TODO: submissions are not sent anywhere yet; wire this to a Laravel
|
|
* endpoint before going live.
|
|
*/
|
|
function ConsultationRequestForm({ t }: { t: Content }) {
|
|
const shared = useContent(common);
|
|
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">
|
|
<Eyebrow icon="quick_reference">{form.eyebrow}</Eyebrow>
|
|
<h2 className="font-display text-headline-lg text-on-surface pt-2 font-bold">
|
|
{form.title}
|
|
</h2>
|
|
<p className="text-on-surface-variant pt-2 text-base">
|
|
{form.description}
|
|
</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"
|
|
/>
|
|
<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>
|
|
</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 }) {
|
|
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
|
|
src="/images/probindo/map-jakarta.png"
|
|
alt={t.location.mapAlt}
|
|
loading="lazy"
|
|
className="absolute inset-0 h-full w-full object-cover dark:brightness-75 dark:hue-rotate-180 dark:invert"
|
|
/>
|
|
<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) => (
|
|
<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"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<IconTile icon={office.icon} tone="solid" />
|
|
<div>
|
|
<span className="text-primary font-mono text-[11px] font-semibold uppercase">
|
|
{office.district}
|
|
</span>
|
|
<h3 className="font-display text-on-surface text-lg font-bold">
|
|
{office.name}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
<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>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|