386 lines
19 KiB
TypeScript
386 lines
19 KiB
TypeScript
import { Head, Link } from '@inertiajs/react';
|
|
import { ButtonLink } from '@/components/site/button';
|
|
import Card from '@/components/site/card';
|
|
import Eyebrow from '@/components/site/eyebrow';
|
|
import Glow, { GridPattern } from '@/components/site/glow';
|
|
import Icon from '@/components/site/icon';
|
|
import IconTile from '@/components/site/icon-tile';
|
|
import OdooPartnerBadge from '@/components/site/odoo-partner-badge';
|
|
import Pill from '@/components/site/pill';
|
|
import RichText from '@/components/site/rich-text';
|
|
import Section from '@/components/site/section';
|
|
import SectionHeader from '@/components/site/section-header';
|
|
import ConsultationSection from '@/components/site/sections/consultation-section';
|
|
import ServiceGrid from '@/components/site/sections/service-grid';
|
|
import StatCard from '@/components/site/stat-card';
|
|
import { homeContent } from '@/content/home';
|
|
import { fillCounts } from '@/data/projects';
|
|
import { useContent } from '@/hooks/use-locale';
|
|
import { usePortfolio } from '@/hooks/use-portfolio';
|
|
import { about, clients, services } from '@/routes';
|
|
|
|
type Content = typeof homeContent.id;
|
|
|
|
export default function Home() {
|
|
const stats = usePortfolio();
|
|
const t = fillCounts(useContent(homeContent), stats);
|
|
|
|
return (
|
|
<>
|
|
<Head title={t.title} />
|
|
<Hero t={t} />
|
|
|
|
<Section tone="muted">
|
|
<div className="flex flex-col gap-12">
|
|
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4 lg:gap-6">
|
|
{t.stats.map((stat) => (
|
|
<StatCard key={stat.label} {...stat} />
|
|
))}
|
|
</div>
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex flex-col justify-between gap-2 md:flex-row md:items-center">
|
|
<div className="flex flex-col gap-1">
|
|
<Eyebrow>{t.sectors.eyebrow}</Eyebrow>
|
|
<p className="text-on-surface-variant text-xs">
|
|
{t.sectors.description}
|
|
</p>
|
|
</div>
|
|
<span className="text-on-surface-variant font-mono text-[11px]">
|
|
{t.sectors.since}
|
|
</span>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
|
{stats.topClients(10).map((client) => (
|
|
<div
|
|
key={client}
|
|
className="border-outline-variant/40 bg-surface-container text-on-surface hover:border-primary/50 hover:bg-surface-container-high flex min-h-16 items-center justify-center rounded-xl border px-4 py-3 text-center font-mono text-xs font-medium transition-all"
|
|
>
|
|
{client}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<ButtonLink
|
|
href={clients()}
|
|
variant="ghost"
|
|
size="inline"
|
|
trailingIcon="arrow_forward"
|
|
className="self-start"
|
|
>
|
|
{t.sectors.more}
|
|
</ButtonLink>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
<Section>
|
|
<div className="grid grid-cols-1 items-start gap-10 lg:grid-cols-12">
|
|
<div className="flex flex-col gap-4 lg:col-span-5">
|
|
<Eyebrow icon="history_edu">
|
|
{t.profile.eyebrow}
|
|
</Eyebrow>
|
|
<h2 className="font-display text-on-surface text-3xl leading-tight font-extrabold lg:text-4xl">
|
|
{t.profile.title}
|
|
</h2>
|
|
<div className="bg-paj-crimson my-1 h-1 w-20 rounded-full" />
|
|
{t.profile.paragraphs.map((paragraph) => (
|
|
<p
|
|
key={paragraph}
|
|
className="text-on-surface-variant text-sm leading-relaxed"
|
|
>
|
|
<RichText text={paragraph} />
|
|
</p>
|
|
))}
|
|
<ButtonLink
|
|
href={about()}
|
|
variant="ghost"
|
|
size="inline"
|
|
trailingIcon="arrow_forward"
|
|
className="self-start"
|
|
>
|
|
{t.profile.more}
|
|
</ButtonLink>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-6 lg:col-span-7">
|
|
{t.profile.statements.map((statement) => (
|
|
<Card
|
|
key={statement.label}
|
|
padding="lg"
|
|
interactive
|
|
className="overflow-hidden"
|
|
>
|
|
<Glow className="-top-12 -right-12 h-40 w-40 blur-2xl" />
|
|
<div className="relative flex items-start gap-4">
|
|
<IconTile icon={statement.icon} size="md" />
|
|
<div className="flex flex-col gap-2">
|
|
<span className="text-primary font-mono text-xs font-bold tracking-wider uppercase">
|
|
{statement.label}
|
|
</span>
|
|
<blockquote className="border-primary/60 text-on-surface border-l-2 pl-3 text-sm leading-relaxed italic sm:text-base">
|
|
“{statement.text}”
|
|
</blockquote>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
<Section id="jasa-layanan" tone="muted">
|
|
<div className="flex flex-col gap-12">
|
|
<SectionHeader
|
|
align="split"
|
|
eyebrow={t.services.eyebrow}
|
|
eyebrowIcon="apps"
|
|
title={t.services.title}
|
|
description={t.services.description}
|
|
/>
|
|
<OdooSpotlight t={t} />
|
|
<ServiceGrid />
|
|
</div>
|
|
</Section>
|
|
|
|
<ConsultationSection
|
|
id="konsultasi"
|
|
source="home"
|
|
title={t.consultation.title}
|
|
description={t.consultation.description}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function Hero({ t }: { t: Content }) {
|
|
return (
|
|
<Section
|
|
tone="muted"
|
|
background={
|
|
<>
|
|
<GridPattern />
|
|
<Glow className="-top-40 -left-20 h-[640px] w-[640px] blur-[140px]" />
|
|
<Glow className="bg-primary/15 top-1/4 -right-20 h-[560px] w-[560px] blur-[150px]" />
|
|
</>
|
|
}
|
|
>
|
|
<div className="grid grid-cols-1 items-center gap-12 lg:grid-cols-12">
|
|
<div className="flex flex-col gap-6 lg:col-span-6">
|
|
<Pill pulse>{t.hero.badge}</Pill>
|
|
<div className="flex flex-col gap-4">
|
|
<h1 className="font-display text-on-surface text-4xl leading-[1.12] font-extrabold tracking-tight sm:text-5xl lg:text-6xl">
|
|
<RichText text={t.hero.title} />
|
|
</h1>
|
|
<p className="text-on-surface-variant text-base leading-relaxed lg:text-lg">
|
|
<RichText text={t.hero.description} />
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-4 pt-2">
|
|
<ButtonLink
|
|
href="#konsultasi"
|
|
trailingIcon="arrow_forward"
|
|
>
|
|
{t.hero.primaryAction}
|
|
</ButtonLink>
|
|
<ButtonLink
|
|
href={services()}
|
|
variant="secondary"
|
|
icon="layers"
|
|
>
|
|
{t.hero.secondaryAction}
|
|
</ButtonLink>
|
|
</div>
|
|
<div className="flex items-center gap-3 pt-2">
|
|
<div className="flex -space-x-2">
|
|
{['KS', 'ME', 'TP'].map((initials) => (
|
|
<div
|
|
key={initials}
|
|
className="border-outline-variant bg-surface-container-high text-primary flex h-8 w-8 items-center justify-center rounded-full border font-mono text-[11px] font-bold shadow-sm"
|
|
>
|
|
{initials}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<p className="text-on-surface-variant text-xs">
|
|
{t.hero.trustBefore}{' '}
|
|
<Link
|
|
href={clients()}
|
|
className="text-on-surface hover:text-primary font-semibold"
|
|
>
|
|
{t.hero.trustLink}
|
|
</Link>{' '}
|
|
{t.hero.trustAfter}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="relative lg:col-span-6">
|
|
<OperationsConsole t={t} />
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
);
|
|
}
|
|
|
|
/** Decorative dashboard mock that illustrates an automated ERP pipeline. */
|
|
function OperationsConsole({ t }: { t: Content }) {
|
|
return (
|
|
<div aria-hidden="true" className="relative">
|
|
<div className="from-paj-crimson/30 via-primary/15 absolute -inset-1 rounded-2xl bg-gradient-to-tr to-transparent blur-2xl" />
|
|
<div className="border-outline-variant/60 bg-surface-container-low relative flex w-full flex-col overflow-hidden rounded-2xl border shadow-2xl">
|
|
<div className="border-outline-variant/40 bg-surface-container-high flex items-center justify-between border-b px-4 py-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="bg-primary/80 h-3 w-3 rounded-full" />
|
|
<span className="h-3 w-3 rounded-full bg-amber-400/80" />
|
|
<span className="h-3 w-3 rounded-full bg-emerald-400/80" />
|
|
<span className="text-on-surface-variant ml-2 font-mono text-[11px]">
|
|
paj-core-suite.internal // v4.20.1
|
|
</span>
|
|
</div>
|
|
<span className="border-primary/30 bg-surface-container-lowest text-primary inline-flex items-center gap-1 rounded border px-2 py-0.5 font-mono text-[11px]">
|
|
<span className="bg-primary h-1.5 w-1.5 animate-pulse rounded-full" />
|
|
LIVE 14ms
|
|
</span>
|
|
</div>
|
|
<div className="bg-surface-container-lowest/90 flex flex-col gap-4 p-6">
|
|
<div className="grid grid-cols-3 gap-3">
|
|
{t.console.metrics.map((metric) => (
|
|
<div
|
|
key={metric.label}
|
|
className="border-outline-variant/40 bg-surface-container flex flex-col rounded-xl border p-3"
|
|
>
|
|
<span className="text-on-surface-variant text-xs">
|
|
{metric.label}
|
|
</span>
|
|
<span
|
|
className={
|
|
metric.accent
|
|
? 'font-display text-primary mt-1 text-xl font-bold'
|
|
: 'font-display text-on-surface mt-1 text-xl font-bold'
|
|
}
|
|
>
|
|
{metric.value}
|
|
</span>
|
|
<span className="text-on-surface-variant mt-0.5 font-mono text-[10px]">
|
|
{metric.note}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="border-outline-variant/40 bg-surface-container flex flex-col gap-2 rounded-xl border p-4">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span className="font-display text-on-surface text-xs font-semibold">
|
|
{t.console.chartTitle}
|
|
</span>
|
|
<span className="text-primary font-mono text-[11px]">
|
|
PostgreSQL • Odoo XML-RPC
|
|
</span>
|
|
</div>
|
|
<Sparkline />
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<div className="text-on-surface-variant flex items-center justify-between px-1 font-mono text-[11px]">
|
|
<span>ENTERPRISE EVENT LOG</span>
|
|
<span>SLA STATUS</span>
|
|
</div>
|
|
{t.console.events.map((event) => (
|
|
<div
|
|
key={event.text}
|
|
className="border-outline-variant/30 bg-surface-container flex items-center justify-between gap-2 rounded-lg border p-2.5"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Icon
|
|
name={event.icon}
|
|
className="text-primary text-sm"
|
|
/>
|
|
<span className="text-on-surface font-mono text-[11px]">
|
|
{event.text}
|
|
</span>
|
|
</div>
|
|
<span className="bg-primary/15 text-primary shrink-0 rounded px-2 py-0.5 font-mono text-[10px] font-semibold">
|
|
{event.status}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Sparkline() {
|
|
const line =
|
|
'M 0 50 Q 30 45, 60 30 T 120 40 T 180 20 T 240 35 T 300 12 T 380 16';
|
|
|
|
return (
|
|
<svg
|
|
className="h-20 w-full overflow-visible pt-1"
|
|
fill="none"
|
|
preserveAspectRatio="none"
|
|
viewBox="0 0 380 70"
|
|
>
|
|
<defs>
|
|
<linearGradient id="spark-glow" x1="0" x2="0" y1="0" y2="1">
|
|
<stop offset="0%" stopColor="#9E1B22" stopOpacity="0.45" />
|
|
<stop offset="60%" stopColor="#FF4D58" stopOpacity="0.12" />
|
|
<stop offset="100%" stopColor="#FF4D58" stopOpacity="0" />
|
|
</linearGradient>
|
|
</defs>
|
|
<path d={`${line} L 380 70 L 0 70 Z`} fill="url(#spark-glow)" />
|
|
<path
|
|
d={line}
|
|
className="stroke-primary"
|
|
strokeLinecap="round"
|
|
strokeWidth="2.5"
|
|
/>
|
|
<circle cx="380" cy="16" r="3.5" className="fill-primary" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function OdooSpotlight({ t }: { t: Content }) {
|
|
return (
|
|
<div className="border-primary-container/40 from-surface-container-low via-surface-container to-surface-container-low relative overflow-hidden rounded-3xl border bg-gradient-to-r p-6 shadow-xl sm:p-8">
|
|
<Glow className="-top-24 -right-24 h-80 w-80 blur-3xl" />
|
|
<div className="relative grid grid-cols-1 items-center gap-8 lg:grid-cols-12">
|
|
<div className="flex flex-col gap-4 lg:col-span-8">
|
|
<OdooPartnerBadge className="self-start" />
|
|
<h3 className="font-display text-on-surface text-2xl font-extrabold lg:text-3xl">
|
|
{t.odoo.title}
|
|
<span className="text-on-surface-variant mt-1 block font-sans text-xl font-normal lg:text-2xl">
|
|
{t.odoo.subtitle}
|
|
</span>
|
|
</h3>
|
|
<p className="text-on-surface-variant text-sm leading-relaxed">
|
|
{t.odoo.description}
|
|
</p>
|
|
<div className="flex flex-wrap gap-2 pt-2">
|
|
{t.odoo.modules.map((module) => (
|
|
<span
|
|
key={module}
|
|
className="border-outline-variant/40 bg-surface-container-high text-on-surface rounded-lg border px-3 py-1 font-mono text-xs"
|
|
>
|
|
{module}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="border-outline-variant/50 bg-surface-container-lowest/80 flex flex-col gap-2 rounded-2xl border p-5 lg:col-span-4">
|
|
<span className="text-primary font-mono text-xs font-bold uppercase">
|
|
{t.odoo.cardLabel}
|
|
</span>
|
|
<p className="text-on-surface-variant text-xs">
|
|
{t.odoo.cardText}
|
|
</p>
|
|
<ButtonLink
|
|
href="#konsultasi"
|
|
size="block"
|
|
trailingIcon="arrow_forward"
|
|
className="mt-2 justify-between"
|
|
>
|
|
{t.odoo.cardAction}
|
|
</ButtonLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|