@@ -1,30 +1,32 @@
|
||||
import { Form } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { Button } from '@/components/site/button';
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
FormSuccess,
|
||||
Honeypot,
|
||||
SelectInput,
|
||||
TextInput,
|
||||
} from '@/components/site/form-field';
|
||||
import { common, serviceOptions } from '@/content/common';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
import { store } from '@/routes/consultations';
|
||||
|
||||
/**
|
||||
* Compact consultation request form.
|
||||
*
|
||||
* TODO: submissions are not sent anywhere yet; wire this to a Laravel
|
||||
* endpoint before going live.
|
||||
*/
|
||||
export default function ConsultationForm({ idPrefix }: { idPrefix: string }) {
|
||||
type ConsultationFormProps = {
|
||||
idPrefix: string;
|
||||
/** Which page the request came from, shown in the admin inbox. */
|
||||
source: 'home' | 'services';
|
||||
};
|
||||
|
||||
/** Compact consultation request form, saved to the admin inbox. */
|
||||
export default function ConsultationForm({
|
||||
idPrefix,
|
||||
source,
|
||||
}: ConsultationFormProps) {
|
||||
const t = useContent(common);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setSubmitted(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-outline-variant/60 bg-surface-container-high flex flex-col gap-4 rounded-2xl border p-6 shadow-xl lg:p-7">
|
||||
<div className="flex flex-col">
|
||||
@@ -42,60 +44,82 @@ export default function ConsultationForm({ idPrefix }: { idPrefix: string }) {
|
||||
message={t.form.successMessage}
|
||||
/>
|
||||
) : (
|
||||
<form className="flex flex-col gap-3" onSubmit={handleSubmit}>
|
||||
<Field id={`${idPrefix}-name`} label={t.form.name} required>
|
||||
<TextInput
|
||||
id={`${idPrefix}-name`}
|
||||
name="name"
|
||||
placeholder={t.form.namePlaceholder}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
id={`${idPrefix}-email`}
|
||||
label={t.form.email}
|
||||
required
|
||||
>
|
||||
<TextInput
|
||||
id={`${idPrefix}-email`}
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder={t.form.emailPlaceholder}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
id={`${idPrefix}-phone`}
|
||||
label={t.form.phone}
|
||||
required
|
||||
>
|
||||
<TextInput
|
||||
id={`${idPrefix}-phone`}
|
||||
name="phone"
|
||||
type="tel"
|
||||
placeholder="+62 811 xxxx xxxx"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field id={`${idPrefix}-service`} label={t.form.service}>
|
||||
<SelectInput
|
||||
id={`${idPrefix}-service`}
|
||||
name="service"
|
||||
options={serviceOptions(t)}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
type="submit"
|
||||
size="block"
|
||||
icon="send"
|
||||
className="mt-2"
|
||||
>
|
||||
{t.form.submit}
|
||||
</Button>
|
||||
<p className="text-on-surface-variant text-center text-[11px]">
|
||||
{t.form.nda}
|
||||
</p>
|
||||
</form>
|
||||
<Form
|
||||
{...store.form()}
|
||||
options={{ preserveScroll: true }}
|
||||
onSuccess={() => setSubmitted(true)}
|
||||
className="relative flex flex-col gap-3"
|
||||
>
|
||||
{({ processing, errors }) => (
|
||||
<>
|
||||
<input type="hidden" name="source" value={source} />
|
||||
<Honeypot />
|
||||
<Field
|
||||
id={`${idPrefix}-name`}
|
||||
label={t.form.name}
|
||||
required
|
||||
>
|
||||
<TextInput
|
||||
id={`${idPrefix}-name`}
|
||||
name="name"
|
||||
placeholder={t.form.namePlaceholder}
|
||||
required
|
||||
/>
|
||||
<FieldError message={errors.name} />
|
||||
</Field>
|
||||
<Field
|
||||
id={`${idPrefix}-email`}
|
||||
label={t.form.email}
|
||||
required
|
||||
>
|
||||
<TextInput
|
||||
id={`${idPrefix}-email`}
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder={t.form.emailPlaceholder}
|
||||
required
|
||||
/>
|
||||
<FieldError message={errors.email} />
|
||||
</Field>
|
||||
<Field
|
||||
id={`${idPrefix}-phone`}
|
||||
label={t.form.phone}
|
||||
required
|
||||
>
|
||||
<TextInput
|
||||
id={`${idPrefix}-phone`}
|
||||
name="phone"
|
||||
type="tel"
|
||||
placeholder="+62 811 xxxx xxxx"
|
||||
required
|
||||
/>
|
||||
<FieldError message={errors.phone} />
|
||||
</Field>
|
||||
<Field
|
||||
id={`${idPrefix}-service`}
|
||||
label={t.form.service}
|
||||
>
|
||||
<SelectInput
|
||||
id={`${idPrefix}-service`}
|
||||
name="service"
|
||||
options={serviceOptions(t)}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
type="submit"
|
||||
size="block"
|
||||
icon={processing ? 'progress_activity' : 'send'}
|
||||
disabled={processing}
|
||||
className="mt-2"
|
||||
>
|
||||
{t.form.submit}
|
||||
</Button>
|
||||
<p className="text-on-surface-variant text-center text-[11px]">
|
||||
{t.form.nda}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -39,6 +39,32 @@ export function Field({
|
||||
);
|
||||
}
|
||||
|
||||
/** Validation message under a field. */
|
||||
export function FieldError({ message }: { message?: string }) {
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <p className="text-primary text-xs font-medium">{message}</p>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden field only bots fill in; the server rejects any submission that
|
||||
* has a value here.
|
||||
*/
|
||||
export function Honeypot() {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
aria-hidden="true"
|
||||
className="absolute -left-[9999px] h-0 w-0 opacity-0"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TextInput({
|
||||
className,
|
||||
...props
|
||||
|
||||
@@ -4,11 +4,12 @@ import Icon from '@/components/site/icon';
|
||||
import Pill from '@/components/site/pill';
|
||||
import Section from '@/components/site/section';
|
||||
import { common } from '@/content/common';
|
||||
import { company, offices } from '@/data/site';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
import { useSiteInfo } from '@/hooks/use-site-info';
|
||||
|
||||
type ConsultationSectionProps = {
|
||||
id: string;
|
||||
source: 'home' | 'services';
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
@@ -16,18 +17,20 @@ type ConsultationSectionProps = {
|
||||
/** Closing call-to-action with contact details and the quick form. */
|
||||
export default function ConsultationSection({
|
||||
id,
|
||||
source,
|
||||
title,
|
||||
description,
|
||||
}: ConsultationSectionProps) {
|
||||
const site = useSiteInfo();
|
||||
const t = useContent(common);
|
||||
const contacts = offices.map((office) => ({
|
||||
const contacts = site.offices.map((office) => ({
|
||||
icon: office.icon,
|
||||
title: office.name,
|
||||
lines: [
|
||||
office.building,
|
||||
`${office.street}, ${office.area}`,
|
||||
office.city,
|
||||
`${t.consultation.tel}: ${office.phone.label}`,
|
||||
`${t.consultation.tel}: ${office.phone}`,
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -74,18 +77,18 @@ export default function ConsultationSection({
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
href={`mailto:${company.email.busdev}`}
|
||||
href={`mailto:${site.email.busdev}`}
|
||||
className="text-on-surface-variant hover:text-primary flex items-center gap-2 text-xs"
|
||||
>
|
||||
<Icon
|
||||
name="mail"
|
||||
className="text-primary text-base"
|
||||
/>
|
||||
{company.email.busdev}
|
||||
{site.email.busdev}
|
||||
</a>
|
||||
</div>
|
||||
<div className="w-full lg:col-span-5">
|
||||
<ConsultationForm idPrefix={id} />
|
||||
<ConsultationForm idPrefix={id} source={source} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import Glow from '@/components/site/glow';
|
||||
import Icon from '@/components/site/icon';
|
||||
import Pill from '@/components/site/pill';
|
||||
import Section from '@/components/site/section';
|
||||
import { company } from '@/data/site';
|
||||
import { useSiteInfo } from '@/hooks/use-site-info';
|
||||
|
||||
type CtaBannerProps = {
|
||||
badge: string;
|
||||
@@ -22,9 +22,10 @@ export default function CtaBanner({
|
||||
actions,
|
||||
showContacts = true,
|
||||
}: CtaBannerProps) {
|
||||
const site = useSiteInfo();
|
||||
const contacts = [
|
||||
{ icon: 'call', label: company.phone.label },
|
||||
{ icon: 'mail', label: company.email.busdev },
|
||||
{ icon: 'call', label: site.phone.label },
|
||||
{ icon: 'mail', label: site.email.busdev },
|
||||
{ icon: 'location_on', label: 'Bintaro & Plaza Simatupang, Jakarta' },
|
||||
];
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import { Link } from '@inertiajs/react';
|
||||
import Container from '@/components/site/container';
|
||||
import Icon from '@/components/site/icon';
|
||||
import { common } from '@/content/common';
|
||||
import { company, consultationUrl, officeAddress, offices } from '@/data/site';
|
||||
import { company, consultationUrl, officeAddress } from '@/data/site';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
import { about, clients, contact, services } from '@/routes';
|
||||
import { useSiteInfo } from '@/hooks/use-site-info';
|
||||
import { about, clients, contact, login, services } from '@/routes';
|
||||
|
||||
function FooterHeading({ children }: { children: string }) {
|
||||
return (
|
||||
@@ -15,6 +16,7 @@ function FooterHeading({ children }: { children: string }) {
|
||||
}
|
||||
|
||||
export default function SiteFooter() {
|
||||
const site = useSiteInfo();
|
||||
const t = useContent(common);
|
||||
|
||||
const companyLinks = [
|
||||
@@ -27,6 +29,7 @@ export default function SiteFooter() {
|
||||
{ label: t.footer.links.clients, href: clients.url() },
|
||||
{ label: t.footer.links.audit, href: consultationUrl },
|
||||
{ label: t.footer.links.offices, href: `${contact.url()}#lokasi` },
|
||||
{ label: t.footer.links.admin, href: login.url() },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -50,7 +53,7 @@ export default function SiteFooter() {
|
||||
{t.footer.tagline}
|
||||
</p>
|
||||
<div className="text-on-surface-variant mt-2 flex flex-col gap-4 text-xs">
|
||||
{offices.map((office) => (
|
||||
{site.offices.map((office) => (
|
||||
<div
|
||||
key={office.name}
|
||||
className="flex items-start gap-2"
|
||||
@@ -65,23 +68,23 @@ export default function SiteFooter() {
|
||||
</span>
|
||||
<span>{officeAddress(office)}</span>
|
||||
<a
|
||||
href={office.phone.href}
|
||||
href={office.phoneHref}
|
||||
className="hover:text-primary font-mono transition-colors"
|
||||
>
|
||||
{t.footer.tel} {office.phone.label}
|
||||
{t.footer.tel} {office.phone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<a
|
||||
href={`mailto:${company.email.busdev}`}
|
||||
href={`mailto:${site.email.busdev}`}
|
||||
className="hover:text-primary flex items-center gap-2 font-mono transition-colors"
|
||||
>
|
||||
<Icon
|
||||
name="mail"
|
||||
className="text-primary text-base"
|
||||
/>
|
||||
{company.email.busdev}
|
||||
{site.email.busdev}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,12 +7,14 @@ import Icon from '@/components/site/icon';
|
||||
import LanguageToggle from '@/components/site/language-toggle';
|
||||
import ThemeToggle from '@/components/site/theme-toggle';
|
||||
import { common } from '@/content/common';
|
||||
import { company, consultationUrl, navigation, offices } from '@/data/site';
|
||||
import { consultationUrl, navigation } from '@/data/site';
|
||||
import { useCurrentUrl } from '@/hooks/use-current-url';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
import { useSiteInfo } from '@/hooks/use-site-info';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function SiteHeader() {
|
||||
const site = useSiteInfo();
|
||||
const t = useContent(common);
|
||||
const { isCurrentUrl } = useCurrentUrl();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
@@ -46,20 +48,20 @@ export default function SiteHeader() {
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="text-on-surface-variant hidden flex-col text-right font-mono text-[11px] 2xl:flex">
|
||||
<a
|
||||
href={offices[0].phone.href}
|
||||
href={site.phone.href}
|
||||
className="text-on-surface hover:text-primary flex items-center justify-end gap-1 font-semibold"
|
||||
>
|
||||
<Icon
|
||||
name="call"
|
||||
className="text-primary text-xs"
|
||||
/>
|
||||
{offices[0].phone.label}
|
||||
{site.phone.label}
|
||||
</a>
|
||||
<a
|
||||
href={`mailto:${company.email.busdev}`}
|
||||
href={`mailto:${site.email.busdev}`}
|
||||
className="hover:text-primary"
|
||||
>
|
||||
{company.email.busdev}
|
||||
{site.email.busdev}
|
||||
</a>
|
||||
</div>
|
||||
<LanguageToggle />
|
||||
|
||||
Reference in New Issue
Block a user