Update Konten & bahasa
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-09-28 16:48:14 +07:00
parent 566275348c
commit 183f36f634
29 changed files with 3285 additions and 1427 deletions
+6 -2
View File
@@ -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 &amp; 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={
<>
+28 -27
View File
@@ -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 &amp; 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>
+19 -12
View File
@@ -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