@@ -1,13 +1,17 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { common } from '@/content/common';
|
||||
import { company } from '@/data/site';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
import { home } from '@/routes';
|
||||
|
||||
/** Logo mark with company name, linking back to the home page. */
|
||||
export default function Brand() {
|
||||
const t = useContent(common);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={home()}
|
||||
title="Kembali ke Beranda"
|
||||
title={t.header.backHome}
|
||||
className="group flex items-center gap-3.5"
|
||||
>
|
||||
<div className="border-outline-variant/50 group-hover:border-primary/60 dark:bg-surface-container-low flex items-center justify-center rounded-xl border bg-white p-1.5 transition-all duration-300 group-hover:shadow-[0_0_20px_rgba(163,29,36,0.35)]">
|
||||
@@ -24,7 +28,7 @@ export default function Brand() {
|
||||
</span>
|
||||
<span className="bg-primary h-1.5 w-1.5 animate-pulse rounded-full" />
|
||||
</div>
|
||||
<span className="text-on-surface-variant font-mono text-[11px] font-medium">
|
||||
<span className="text-on-surface-variant hidden font-mono text-[11px] font-medium sm:block">
|
||||
{company.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
SelectInput,
|
||||
TextInput,
|
||||
} from '@/components/site/form-field';
|
||||
import { serviceOptions } from '@/data/site';
|
||||
import { common, serviceOptions } from '@/content/common';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
|
||||
/**
|
||||
* Compact consultation request form.
|
||||
@@ -16,6 +17,7 @@ import { serviceOptions } from '@/data/site';
|
||||
* endpoint before going live.
|
||||
*/
|
||||
export default function ConsultationForm({ idPrefix }: { idPrefix: string }) {
|
||||
const t = useContent(common);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
@@ -27,49 +29,44 @@ export default function ConsultationForm({ idPrefix }: { idPrefix: string }) {
|
||||
<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">
|
||||
<span className="font-display text-on-surface text-base font-bold">
|
||||
Jadwalkan Konsultasi IT
|
||||
{t.form.title}
|
||||
</span>
|
||||
<span className="text-on-surface-variant text-xs">
|
||||
Konfirmasi langsung oleh Lead Architect PAJ dalam 2 jam
|
||||
kerja.
|
||||
{t.form.subtitle}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{submitted ? (
|
||||
<FormSuccess
|
||||
title="Permintaan Terkirim!"
|
||||
message="Tim Lead Consultant PT Probindo Artika Jaya akan segera menghubungi kontak Anda untuk konfirmasi jadwal sesi konsultasi."
|
||||
title={t.form.successTitle}
|
||||
message={t.form.successMessage}
|
||||
/>
|
||||
) : (
|
||||
<form className="flex flex-col gap-3" onSubmit={handleSubmit}>
|
||||
<Field
|
||||
id={`${idPrefix}-name`}
|
||||
label="Nama Lengkap & Perusahaan"
|
||||
required
|
||||
>
|
||||
<Field id={`${idPrefix}-name`} label={t.form.name} required>
|
||||
<TextInput
|
||||
id={`${idPrefix}-name`}
|
||||
name="name"
|
||||
placeholder="Nama Anda (PT ...)"
|
||||
placeholder={t.form.namePlaceholder}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
id={`${idPrefix}-email`}
|
||||
label="Email Perusahaan / Kantor"
|
||||
label={t.form.email}
|
||||
required
|
||||
>
|
||||
<TextInput
|
||||
id={`${idPrefix}-email`}
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="nama@perusahaan.co.id"
|
||||
placeholder={t.form.emailPlaceholder}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
id={`${idPrefix}-phone`}
|
||||
label="No. WhatsApp / Telepon"
|
||||
label={t.form.phone}
|
||||
required
|
||||
>
|
||||
<TextInput
|
||||
@@ -80,14 +77,11 @@ export default function ConsultationForm({ idPrefix }: { idPrefix: string }) {
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
id={`${idPrefix}-service`}
|
||||
label="Layanan Yang Dibutuhkan"
|
||||
>
|
||||
<Field id={`${idPrefix}-service`} label={t.form.service}>
|
||||
<SelectInput
|
||||
id={`${idPrefix}-service`}
|
||||
name="service"
|
||||
options={serviceOptions}
|
||||
options={serviceOptions(t)}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
@@ -96,11 +90,10 @@ export default function ConsultationForm({ idPrefix }: { idPrefix: string }) {
|
||||
icon="send"
|
||||
className="mt-2"
|
||||
>
|
||||
Kirim Permintaan Konsultasi
|
||||
{t.form.submit}
|
||||
</Button>
|
||||
<p className="text-on-surface-variant text-center text-[11px]">
|
||||
Non-Disclosure Agreement (NDA) dapat ditandatangani
|
||||
sebelum audit teknis.
|
||||
{t.form.nda}
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Locale } from '@/hooks/use-locale';
|
||||
import { useLocale } from '@/hooks/use-locale';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const languages: { value: Locale; label: string; name: string }[] = [
|
||||
{ value: 'id', label: 'ID', name: 'Bahasa Indonesia' },
|
||||
{ value: 'en', label: 'EN', name: 'English' },
|
||||
];
|
||||
|
||||
/** Two-option switch between Indonesian and English. */
|
||||
export default function LanguageToggle({ className }: { className?: string }) {
|
||||
const { locale, setLocale } = useLocale();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label="Bahasa / Language"
|
||||
className={cn(
|
||||
'border-outline-variant/50 bg-surface-container-low inline-flex h-10 items-center gap-0.5 rounded-xl border p-1',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{languages.map((language) => (
|
||||
<button
|
||||
key={language.value}
|
||||
type="button"
|
||||
lang={language.value}
|
||||
title={language.name}
|
||||
aria-pressed={locale === language.value}
|
||||
onClick={() => setLocale(language.value)}
|
||||
className={cn(
|
||||
'h-full cursor-pointer rounded-lg px-2.5 font-mono text-xs font-bold transition-colors',
|
||||
locale === language.value
|
||||
? 'bg-primary-container text-white'
|
||||
: 'text-on-surface-variant hover:text-on-surface',
|
||||
)}
|
||||
>
|
||||
{language.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Fragment } from 'react';
|
||||
import { Highlight, Strong } from '@/components/site/page-hero';
|
||||
|
||||
/**
|
||||
* Renders content strings with light inline markup, so translations can
|
||||
* keep emphasis without splitting sentences into pieces:
|
||||
*
|
||||
* - `**text**` → emphasized text
|
||||
* - `==text==` → crimson gradient highlight (hero titles)
|
||||
*/
|
||||
export default function RichText({ text }: { text: string }) {
|
||||
const parts = text.split(/(\*\*.+?\*\*|==.+?==)/g);
|
||||
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, index) => {
|
||||
if (part.startsWith('**') && part.endsWith('**')) {
|
||||
return <Strong key={index}>{part.slice(2, -2)}</Strong>;
|
||||
}
|
||||
|
||||
if (part.startsWith('==') && part.endsWith('==')) {
|
||||
return (
|
||||
<Highlight key={index}>{part.slice(2, -2)}</Highlight>
|
||||
);
|
||||
}
|
||||
|
||||
return <Fragment key={index}>{part}</Fragment>;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,9 @@ 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 { common } from '@/content/common';
|
||||
import { company, offices } from '@/data/site';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
|
||||
type ConsultationSectionProps = {
|
||||
id: string;
|
||||
@@ -17,6 +19,7 @@ export default function ConsultationSection({
|
||||
title,
|
||||
description,
|
||||
}: ConsultationSectionProps) {
|
||||
const t = useContent(common);
|
||||
const contacts = offices.map((office) => ({
|
||||
icon: office.icon,
|
||||
title: office.name,
|
||||
@@ -24,7 +27,7 @@ export default function ConsultationSection({
|
||||
office.building,
|
||||
`${office.street}, ${office.area}`,
|
||||
office.city,
|
||||
`Telp: ${office.phone.label}`,
|
||||
`${t.consultation.tel}: ${office.phone.label}`,
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -39,7 +42,7 @@ export default function ConsultationSection({
|
||||
<div className="grid grid-cols-1 items-center gap-10 lg:grid-cols-12">
|
||||
<div className="flex flex-col gap-6 lg:col-span-7">
|
||||
<Pill tone="brand" icon="verified_user">
|
||||
KONSULTASI TEKNIS & AUDIT INFRASTRUKTUR
|
||||
{t.consultation.badge}
|
||||
</Pill>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="font-display text-on-surface lg:text-headline-xl text-3xl leading-tight font-extrabold tracking-tight">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import FeatureCard from '@/components/site/feature-card';
|
||||
import { officialServices } from '@/data/site';
|
||||
import { common } from '@/content/common';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
|
||||
type ServiceGridProps = {
|
||||
/** `summary` for overviews, `description` for the official wording. */
|
||||
@@ -8,16 +9,18 @@ type ServiceGridProps = {
|
||||
|
||||
/** The eight official PAJ services as a 4-column card grid. */
|
||||
export default function ServiceGrid({ detail = 'summary' }: ServiceGridProps) {
|
||||
const t = useContent(common);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-4">
|
||||
{officialServices.map((service, index) => {
|
||||
{t.services.map((service, index) => {
|
||||
const number = String(index + 1).padStart(2, '0');
|
||||
|
||||
return (
|
||||
<FeatureCard
|
||||
key={service.title}
|
||||
icon={service.icon}
|
||||
tag={`${number} / JASA RESMI`}
|
||||
tag={`${number} / ${t.serviceTag}`}
|
||||
title={`${index + 1}. ${service.title}`}
|
||||
description={
|
||||
<>
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import Container from '@/components/site/container';
|
||||
import Icon from '@/components/site/icon';
|
||||
import {
|
||||
company,
|
||||
consultationUrl,
|
||||
officeAddress,
|
||||
offices,
|
||||
officialServices,
|
||||
} from '@/data/site';
|
||||
import { common } from '@/content/common';
|
||||
import { company, consultationUrl, officeAddress, offices } from '@/data/site';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
import { about, clients, contact, services } from '@/routes';
|
||||
|
||||
const companyLinks = [
|
||||
{ label: 'Tentang Kami', href: about.url() },
|
||||
{ label: 'Visi & Misi Resmi', href: `${about.url()}#visi-misi` },
|
||||
{ label: 'Solusi Odoo ERP', href: `${services.url()}#odoo-erp` },
|
||||
{ label: 'Daftar Klien Industri', href: clients.url() },
|
||||
{ label: 'Permintaan Audit Sistem', href: consultationUrl },
|
||||
{ label: 'Lokasi Kantor PAJ', href: `${contact.url()}#lokasi` },
|
||||
];
|
||||
|
||||
function FooterHeading({ children }: { children: string }) {
|
||||
return (
|
||||
<span className="text-on-surface font-mono text-xs font-bold tracking-wider uppercase">
|
||||
@@ -28,6 +15,20 @@ function FooterHeading({ children }: { children: string }) {
|
||||
}
|
||||
|
||||
export default function SiteFooter() {
|
||||
const t = useContent(common);
|
||||
|
||||
const companyLinks = [
|
||||
{ label: t.footer.links.about, href: about.url() },
|
||||
{
|
||||
label: t.footer.links.visionMission,
|
||||
href: `${about.url()}#visi-misi`,
|
||||
},
|
||||
{ label: t.footer.links.odoo, href: `${services.url()}#odoo-erp` },
|
||||
{ label: t.footer.links.clients, href: clients.url() },
|
||||
{ label: t.footer.links.audit, href: consultationUrl },
|
||||
{ label: t.footer.links.offices, href: `${contact.url()}#lokasi` },
|
||||
];
|
||||
|
||||
return (
|
||||
<footer className="border-outline-variant/40 bg-surface-container-lowest w-full border-t py-16">
|
||||
<Container>
|
||||
@@ -46,7 +47,7 @@ export default function SiteFooter() {
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-on-surface-variant max-w-md text-xs leading-relaxed">
|
||||
{company.tagline}
|
||||
{t.footer.tagline}
|
||||
</p>
|
||||
<div className="text-on-surface-variant mt-2 flex flex-col gap-4 text-xs">
|
||||
{offices.map((office) => (
|
||||
@@ -67,7 +68,7 @@ export default function SiteFooter() {
|
||||
href={office.phone.href}
|
||||
className="hover:text-primary font-mono transition-colors"
|
||||
>
|
||||
Tel. {office.phone.label}
|
||||
{t.footer.tel} {office.phone.label}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,9 +87,11 @@ export default function SiteFooter() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 lg:col-span-4">
|
||||
<FooterHeading>8 Jasa Resmi PAJ</FooterHeading>
|
||||
<FooterHeading>
|
||||
{t.footer.servicesHeading}
|
||||
</FooterHeading>
|
||||
<ul className="text-on-surface-variant grid grid-cols-1 gap-2 text-xs sm:grid-cols-2">
|
||||
{officialServices.map((service, index) => (
|
||||
{t.services.map((service, index) => (
|
||||
<li key={service.title}>
|
||||
<Link
|
||||
href={`${services.url()}#jasa-resmi`}
|
||||
@@ -102,7 +105,7 @@ export default function SiteFooter() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 lg:col-span-3">
|
||||
<FooterHeading>Informasi & Tautan</FooterHeading>
|
||||
<FooterHeading>{t.footer.linksHeading}</FooterHeading>
|
||||
<ul className="text-on-surface-variant flex flex-col gap-2 text-xs">
|
||||
{companyLinks.map((link) => (
|
||||
<li key={link.label}>
|
||||
@@ -120,15 +123,13 @@ export default function SiteFooter() {
|
||||
|
||||
<div className="text-on-surface-variant flex flex-col items-center justify-between gap-4 pt-8 text-xs md:flex-row">
|
||||
<p>
|
||||
© {new Date().getFullYear()} {company.name}. Hak Cipta
|
||||
Dilindungi Undang-Undang.
|
||||
© {new Date().getFullYear()} {company.name}.{' '}
|
||||
{t.footer.rights}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-6 gap-y-1 font-mono text-[11px]">
|
||||
<span>
|
||||
{company.notary} ({company.founded})
|
||||
</span>
|
||||
<span>Est. {company.established}</span>
|
||||
<span className="text-primary">•</span>
|
||||
<span>Jakarta Selatan, Indonesia</span>
|
||||
<span>{t.footer.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -4,12 +4,16 @@ import Brand from '@/components/site/brand';
|
||||
import { ButtonLink } from '@/components/site/button';
|
||||
import Container from '@/components/site/container';
|
||||
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 { useCurrentUrl } from '@/hooks/use-current-url';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function SiteHeader() {
|
||||
const t = useContent(common);
|
||||
const { isCurrentUrl } = useCurrentUrl();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const closeMenu = () => setMenuOpen(false);
|
||||
@@ -20,12 +24,12 @@ export default function SiteHeader() {
|
||||
<Brand />
|
||||
|
||||
<nav
|
||||
aria-label="Navigasi utama"
|
||||
aria-label={t.header.mainNav}
|
||||
className="hidden items-center gap-8 lg:flex"
|
||||
>
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'hover:border-primary hover:text-on-surface border-b-2 pb-1 text-sm font-medium transition-all',
|
||||
@@ -34,12 +38,12 @@ export default function SiteHeader() {
|
||||
: 'text-on-surface-variant border-transparent',
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
{t.nav[item.key]}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<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}
|
||||
@@ -58,18 +62,21 @@ export default function SiteHeader() {
|
||||
{company.email.busdev}
|
||||
</a>
|
||||
</div>
|
||||
<LanguageToggle />
|
||||
<ThemeToggle />
|
||||
<ButtonLink
|
||||
href={consultationUrl}
|
||||
size="sm"
|
||||
icon="support_agent"
|
||||
className="hidden sm:inline-flex"
|
||||
className="hidden md:inline-flex"
|
||||
>
|
||||
Konsultasi IT
|
||||
{t.header.consult}
|
||||
</ButtonLink>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={menuOpen ? 'Tutup menu' : 'Buka menu'}
|
||||
aria-label={
|
||||
menuOpen ? t.header.closeMenu : t.header.openMenu
|
||||
}
|
||||
aria-expanded={menuOpen}
|
||||
onClick={() => setMenuOpen((open) => !open)}
|
||||
className="border-outline-variant/50 bg-surface-container-low text-on-surface inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-xl border lg:hidden"
|
||||
@@ -84,13 +91,13 @@ export default function SiteHeader() {
|
||||
|
||||
{menuOpen && (
|
||||
<nav
|
||||
aria-label="Navigasi utama"
|
||||
aria-label={t.header.mainNav}
|
||||
className="border-outline-variant/40 bg-surface border-t lg:hidden"
|
||||
>
|
||||
<Container className="flex flex-col gap-1 py-4">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
onClick={closeMenu}
|
||||
className={cn(
|
||||
@@ -100,7 +107,7 @@ export default function SiteHeader() {
|
||||
: 'text-on-surface-variant hover:bg-surface-container hover:text-on-surface',
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
{t.nav[item.key]}
|
||||
</Link>
|
||||
))}
|
||||
<ButtonLink
|
||||
@@ -108,9 +115,9 @@ export default function SiteHeader() {
|
||||
size="sm"
|
||||
icon="support_agent"
|
||||
onClick={closeMenu}
|
||||
className="mt-2 sm:hidden"
|
||||
className="mt-2 md:hidden"
|
||||
>
|
||||
Konsultasi IT
|
||||
{t.header.consult}
|
||||
</ButtonLink>
|
||||
</Container>
|
||||
</nav>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import Icon from '@/components/site/icon';
|
||||
import { common } from '@/content/common';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/** Switches the whole app between light and dark mode. */
|
||||
export default function ThemeToggle({ className }: { className?: string }) {
|
||||
const t = useContent(common);
|
||||
const { resolvedAppearance, updateAppearance } = useAppearance();
|
||||
const isDark = resolvedAppearance === 'dark';
|
||||
const label = isDark ? 'Ganti ke mode terang' : 'Ganti ke mode gelap';
|
||||
const label = isDark ? t.header.toLight : t.header.toDark;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user