@@ -1,4 +1,4 @@
|
||||
import { Head } from '@inertiajs/react';
|
||||
import { Head, usePage } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
import { Button, ButtonLink } from '@/components/site/button';
|
||||
import Card from '@/components/site/card';
|
||||
@@ -13,21 +13,22 @@ import Section from '@/components/site/section';
|
||||
import SectionHeader from '@/components/site/section-header';
|
||||
import CtaBanner from '@/components/site/sections/cta-banner';
|
||||
import { clientsContent } from '@/content/clients';
|
||||
import type { ProjectSector } from '@/data/projects';
|
||||
import {
|
||||
projectsBySector,
|
||||
projectsInProgram,
|
||||
yearRange,
|
||||
} from '@/data/projects';
|
||||
import { company, consultationUrl, officeAddress, offices } from '@/data/site';
|
||||
import type { Portfolio } from '@/data/projects';
|
||||
import { fillCounts, yearRange } from '@/data/projects';
|
||||
import { company, consultationUrl, officeAddress } from '@/data/site';
|
||||
import { useContent } from '@/hooks/use-locale';
|
||||
import { usePortfolio } from '@/hooks/use-portfolio';
|
||||
import { useSiteInfo } from '@/hooks/use-site-info';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { contact } from '@/routes';
|
||||
import type { LocalizedTestimonial, ProjectSector } from '@/types';
|
||||
|
||||
type Content = typeof clientsContent.id;
|
||||
|
||||
export default function Clients() {
|
||||
const t = useContent(clientsContent);
|
||||
const stats = usePortfolio();
|
||||
const t = fillCounts(useContent(clientsContent), stats);
|
||||
const site = useSiteInfo();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -94,9 +95,9 @@ export default function Clients() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<SectorPortfolio t={t} />
|
||||
<SectorPortfolio t={t} stats={stats} />
|
||||
|
||||
<FeaturedPrograms t={t} />
|
||||
<FeaturedPrograms t={t} stats={stats} />
|
||||
|
||||
<Section tone="muted">
|
||||
<div className="flex flex-col gap-12">
|
||||
@@ -194,28 +195,28 @@ export default function Clients() {
|
||||
{t.office.description}
|
||||
</p>
|
||||
<div className="text-on-surface mt-2 flex flex-col gap-2 font-mono text-sm">
|
||||
{offices.map((office) => (
|
||||
{site.offices.map((office) => (
|
||||
<a
|
||||
key={office.name}
|
||||
href={office.phone.href}
|
||||
href={office.phoneHref}
|
||||
className="hover:text-primary flex items-center gap-2"
|
||||
>
|
||||
<Icon
|
||||
name="call"
|
||||
className="text-primary text-base"
|
||||
/>
|
||||
{office.phone.label}
|
||||
{office.phone}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href={`mailto:${company.email.busdev}`}
|
||||
href={`mailto:${site.email.busdev}`}
|
||||
className="hover:text-primary flex items-center gap-2"
|
||||
>
|
||||
<Icon
|
||||
name="mail"
|
||||
className="text-primary text-base"
|
||||
/>
|
||||
{company.email.busdev}
|
||||
{site.email.busdev}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,7 +228,7 @@ export default function Clients() {
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
<div className="relative z-10 grid w-full grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{offices.map((office) => (
|
||||
{site.offices.map((office) => (
|
||||
<div
|
||||
key={office.name}
|
||||
className="bg-surface-container-lowest/90 rounded-xl p-4 shadow-lg backdrop-blur-md"
|
||||
@@ -250,11 +251,11 @@ export default function Clients() {
|
||||
|
||||
const PREVIEW_COUNT = 8;
|
||||
|
||||
function SectorPortfolio({ t }: { t: Content }) {
|
||||
function SectorPortfolio({ t, stats }: { t: Content; stats: Portfolio }) {
|
||||
const sectors: ProjectSector[] = ['government', 'private', 'soe'];
|
||||
const [sector, setSector] = useState<ProjectSector>('government');
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const items = projectsBySector(sector);
|
||||
const items = stats.projectsBySector(sector);
|
||||
const visible = expanded ? items : items.slice(0, PREVIEW_COUNT);
|
||||
|
||||
const selectSector = (next: ProjectSector) => {
|
||||
@@ -289,7 +290,7 @@ function SectorPortfolio({ t }: { t: Content }) {
|
||||
)}
|
||||
>
|
||||
{t.portfolio.sectors[option]} (
|
||||
{projectsBySector(option).length})
|
||||
{stats.projectsBySector(option).length})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -360,7 +361,7 @@ function SectorPortfolio({ t }: { t: Content }) {
|
||||
|
||||
const RELATED_PREVIEW = 4;
|
||||
|
||||
function FeaturedPrograms({ t }: { t: Content }) {
|
||||
function FeaturedPrograms({ t, stats }: { t: Content; stats: Portfolio }) {
|
||||
return (
|
||||
<Section>
|
||||
<div className="flex flex-col gap-12">
|
||||
@@ -371,7 +372,7 @@ function FeaturedPrograms({ t }: { t: Content }) {
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||
{t.cases.items.map((item, index) => {
|
||||
const related = projectsInProgram(item.program);
|
||||
const related = stats.projectsInProgram(item.program);
|
||||
const remaining = related.length - RELATED_PREVIEW;
|
||||
|
||||
return (
|
||||
@@ -456,9 +457,13 @@ function FeaturedPrograms({ t }: { t: Content }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Hidden until real client testimonials are added to the content file. */
|
||||
/** Shown only once published testimonials exist (admin → Testimoni). */
|
||||
function Testimonials({ t }: { t: Content }) {
|
||||
if (t.testimonials.items.length === 0) {
|
||||
const { testimonials } = usePage<{
|
||||
testimonials: LocalizedTestimonial[];
|
||||
}>().props;
|
||||
|
||||
if (testimonials.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -472,9 +477,9 @@ function Testimonials({ t }: { t: Content }) {
|
||||
description={t.testimonials.description}
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
{t.testimonials.items.map((testimonial) => (
|
||||
{testimonials.map((testimonial) => (
|
||||
<Card
|
||||
key={testimonial.initials}
|
||||
key={testimonial.id}
|
||||
className="flex flex-col justify-between gap-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
@@ -492,7 +497,7 @@ function Testimonials({ t }: { t: Content }) {
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-surface-container text-primary flex h-10 w-10 items-center justify-center rounded-full font-mono font-bold">
|
||||
{testimonial.initials}
|
||||
{initials(testimonial.role)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-on-surface text-sm font-bold">
|
||||
@@ -511,6 +516,14 @@ function Testimonials({ t }: { t: Content }) {
|
||||
);
|
||||
}
|
||||
|
||||
const initials = (text: string) =>
|
||||
text
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((word) => word[0].toUpperCase())
|
||||
.join('');
|
||||
|
||||
function RetentionGauge({ t }: { t: Content }) {
|
||||
return (
|
||||
<div className="bg-surface-container flex w-full flex-col items-center justify-center rounded-xl p-6 lg:w-96">
|
||||
|
||||
Reference in New Issue
Block a user