Admin Panel
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-09-29 14:57:37 +07:00
parent 04fbddba13
commit 8c6dad2a31
94 changed files with 4864 additions and 1151 deletions
+40 -27
View File
@@ -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">