Files
Moniki_uslugi/apps/web/src/components/pages/CmsPageBody.tsx
T

399 lines
15 KiB
TypeScript

import {
ComingSoon,
InfoList,
SourceNote,
} from "@/components/organization/OrganizationLayout";
import { RichText } from "@/components/pages/RichText";
import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { Link } from "@/i18n/routing";
import type { SitePage } from "@/lib/site-pages";
import { getTranslations } from "next-intl/server";
import type { ReactNode } from "react";
function PageAnchor({
href,
children,
className,
}: {
href: string;
children: ReactNode;
className?: string;
}) {
if (href.startsWith("/")) {
return (
<Link href={href} className={className}>
{children}
</Link>
);
}
return (
<a href={href} target="_blank" rel="noopener noreferrer" className={className}>
{children}
</a>
);
}
function PeopleBlock({ page }: { page: SitePage }) {
const hasPhotos = page.people.some((person) => person.photo);
if (hasPhotos) {
return (
<ul className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{page.people.map((person) => (
<li
key={`${person.name}-${person.position}`}
className="min-w-0 overflow-hidden rounded-[var(--radius)] border border-[var(--border)] bg-[var(--surface)]"
>
{person.photo ? (
<div className="relative aspect-[3/4] overflow-hidden bg-[var(--neutral-100)]">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={person.photo}
alt={person.name}
width={720}
height={960}
className="absolute inset-0 h-full w-full max-w-none object-cover object-center"
/>
</div>
) : null}
<div className="p-4">
{person.position ? (
<p className="break-words text-sm text-[var(--neutral-500)]">{person.position}</p>
) : null}
<h2 className="mt-1 break-words text-base font-semibold text-[var(--neutral-900)]">
{person.name}
</h2>
{person.degree ? (
<p className="mt-1 break-words text-sm text-[var(--neutral-700)]">{person.degree}</p>
) : null}
</div>
</li>
))}
</ul>
);
}
const hasSchedule = page.people.some((person) => person.hours || person.address);
if (!hasSchedule) {
return (
<ul className="space-y-3">
{page.people.map((person) => (
<li key={`${person.name}-${person.position}`}>
<p className="font-medium text-[var(--neutral-900)]">{person.name}</p>
{person.position ? (
<p className="text-sm text-[var(--neutral-600)]">{person.position}</p>
) : null}
</li>
))}
</ul>
);
}
return <ReceptionPeople page={page} />;
}
async function ReceptionPeople({ page }: { page: SitePage }) {
const t = await getTranslations("organization");
return (
<div className="min-w-0">
<ul className="space-y-3 md:hidden">
{page.people.map((row) => (
<li
key={`${row.name}-${row.position}`}
className="rounded-[var(--radius)] border border-[var(--border)] bg-[var(--surface)] p-4"
>
<p className="whitespace-pre-line font-semibold text-[var(--neutral-900)]">
{row.position}
</p>
<p className="mt-1 text-[var(--neutral-800)]">{row.name}</p>
<dl className="mt-3 space-y-2 text-sm">
<div>
<dt className="text-[var(--neutral-500)]">{t("reception.hours")}</dt>
<dd className="mt-0.5 whitespace-pre-line text-[var(--neutral-800)]">
{row.hours || "—"}
</dd>
</div>
<div>
<dt className="text-[var(--neutral-500)]">{t("address")}</dt>
<dd className="mt-0.5 whitespace-pre-line text-[var(--neutral-800)]">
{row.address || "—"}
</dd>
</div>
</dl>
</li>
))}
</ul>
<div className="hidden overflow-x-auto rounded-[var(--radius)] border border-[var(--border)] bg-[var(--surface)] md:block">
<table className="w-full min-w-[36rem] border-collapse text-left text-sm">
<thead className="bg-[var(--neutral-50)]">
<tr>
<th scope="col" className="px-4 py-3 font-semibold text-[var(--neutral-800)]">
{t("reception.position")}
</th>
<th scope="col" className="px-4 py-3 font-semibold text-[var(--neutral-800)]">
{t("reception.hours")}
</th>
<th scope="col" className="px-4 py-3 font-semibold text-[var(--neutral-800)]">
{t("address")}
</th>
</tr>
</thead>
<tbody>
{page.people.map((row) => (
<tr key={`${row.name}-${row.position}`} className="border-t border-[var(--border)] align-top">
<td className="px-4 py-3 text-[var(--neutral-800)]">
<p className="whitespace-pre-line font-medium">{row.position}</p>
<p className="mt-1 whitespace-pre-line">{row.name}</p>
</td>
<td className="whitespace-pre-line px-4 py-3 text-[var(--neutral-800)]">
{row.hours || "—"}
</td>
<td className="whitespace-pre-line px-4 py-3 text-[var(--neutral-800)]">
{row.address || "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
async function CardsBlock({ page }: { page: SitePage }) {
const t = await getTranslations("organization");
return (
<div className="space-y-6">
{page.cards.map((item) => (
<Card key={item.title}>
<div className="flex flex-wrap items-center justify-between gap-2">
<h2 className="text-lg font-semibold text-[var(--neutral-900)]">{item.title}</h2>
{item.status ? (
<p className="text-sm font-medium text-[var(--brand-700)]">{item.status}</p>
) : null}
</div>
{item.role ? <p className="mt-2 text-sm text-[var(--text-muted)]">{item.role}</p> : null}
<dl className="mt-4 grid gap-3 text-sm sm:grid-cols-2">
{item.number ? (
<div>
<dt className="text-[var(--neutral-500)]">{t("licenseNumber")}</dt>
<dd className="mt-1 font-medium text-[var(--neutral-800)]">{item.number}</dd>
</div>
) : null}
{item.issuedAt ? (
<div>
<dt className="text-[var(--neutral-500)]">{t("issuedAt")}</dt>
<dd className="mt-1 text-[var(--neutral-800)]">{item.issuedAt}</dd>
</div>
) : null}
{item.authority ? (
<div className="sm:col-span-2">
<dt className="text-[var(--neutral-500)]">{t("authority")}</dt>
<dd className="mt-1 text-[var(--neutral-800)]">{item.authority}</dd>
</div>
) : null}
{item.activity ? (
<div className="sm:col-span-2">
<dt className="text-[var(--neutral-500)]">{t("activity")}</dt>
<dd className="mt-1 text-[var(--neutral-800)]">{item.activity}</dd>
</div>
) : null}
{item.phone ? (
<div>
<dt className="text-[var(--neutral-500)]">{t("phone")}</dt>
<dd className="mt-1 text-[var(--neutral-800)]">{item.phone}</dd>
</div>
) : null}
{item.address ? (
<div className="sm:col-span-2">
<dt className="text-[var(--neutral-500)]">{t("address")}</dt>
<dd className="mt-1 text-[var(--neutral-800)]">{item.address}</dd>
</div>
) : null}
{item.email ? (
<div className="sm:col-span-2">
<dt className="text-[var(--neutral-500)]">{t("email")}</dt>
<dd className="mt-1">
<a href={`mailto:${item.email}`} className="text-[var(--brand-700)] hover:underline">
{item.email}
</a>
</dd>
</div>
) : null}
</dl>
{item.extract ? <p className="mt-4 text-sm text-[var(--text-muted)]">{item.extract}</p> : null}
{item.href ? (
<PageAnchor href={item.href} className="mt-4 inline-flex">
{item.linkLabel && !item.number && !item.authority ? (
<span className="text-sm font-medium text-[var(--brand-700)] hover:underline">
{item.linkLabel}
</span>
) : (
<Button variant="outline" size="sm">
{item.linkLabel || item.href}
</Button>
)}
</PageAnchor>
) : null}
</Card>
))}
</div>
);
}
async function TableBlock({ page }: { page: SitePage }) {
const t = await getTranslations("legal.discounts");
return (
<div className="min-w-0 space-y-4">
{page.tableCaption ? (
<p className="text-[var(--neutral-700)]">{page.tableCaption}</p>
) : null}
<ul className="space-y-3 md:hidden">
{page.tableRows.map((row) => (
<li
key={`${row.category}-${row.condition}-${row.discount}`}
className="rounded-[var(--radius)] border border-[var(--border)] bg-[var(--surface)] p-4"
>
<p className="font-semibold text-[var(--neutral-900)]">{row.category}</p>
{row.condition ? (
<p className="mt-1 text-sm text-[var(--neutral-700)]">{row.condition}</p>
) : null}
<dl className="mt-3 space-y-2 text-sm">
<div>
<dt className="text-[var(--neutral-500)]">{t("discount")}</dt>
<dd className="mt-0.5 font-medium text-[var(--brand-700)]">{row.discount}</dd>
</div>
<div>
<dt className="text-[var(--neutral-500)]">{t("documents")}</dt>
<dd className="mt-0.5 text-[var(--neutral-800)]">{row.documents}</dd>
</div>
</dl>
</li>
))}
</ul>
<div className="hidden overflow-x-auto rounded-[var(--radius)] border border-[var(--border)] bg-[var(--surface)] md:block">
<table className="w-full min-w-[40rem] border-collapse text-left text-sm">
<caption className="sr-only">{page.tableCaption || t("heading")}</caption>
<thead className="bg-[var(--neutral-50)]">
<tr>
<th scope="col" className="px-4 py-3 font-semibold text-[var(--neutral-800)]">
{t("category")}
</th>
<th scope="col" className="px-4 py-3 font-semibold text-[var(--neutral-800)]">
{t("discount")}
</th>
<th scope="col" className="px-4 py-3 font-semibold text-[var(--neutral-800)]">
{t("documents")}
</th>
</tr>
</thead>
<tbody>
{page.tableRows.map((row) => (
<tr
key={`${row.category}-${row.condition}-${row.discount}`}
className="border-t border-[var(--border)] align-top"
>
<th scope="row" className="px-4 py-3 font-medium text-[var(--neutral-800)]">
<p>{row.category}</p>
{row.condition ? (
<p className="mt-1 font-normal text-[var(--neutral-600)]">{row.condition}</p>
) : null}
</th>
<td className="whitespace-nowrap px-4 py-3 font-semibold text-[var(--brand-700)]">
{row.discount}
</td>
<td className="px-4 py-3 text-[var(--neutral-800)]">{row.documents}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
export async function CmsPageBody({ page }: { page: SitePage }) {
const t = await getTranslations("organization");
const ctaLabel = page.ctaLabel || page.ctaHref;
return (
<>
<h1 className="text-3xl font-bold text-[var(--neutral-900)]">{page.heading}</h1>
{page.subtitle ? (
<p className="mt-2 max-w-3xl text-[var(--text-muted)]">{page.subtitle}</p>
) : null}
<div className="mt-8 space-y-8">
{page.tableRows.length === 0 && page.timeline.length === 0 ? (
<RichText value={page.content} />
) : null}
{page.tableRows.length ? <TableBlock page={page} /> : null}
{page.facts.length ? <InfoList items={page.facts} /> : null}
{page.timeline.length ? (
<div>
<h2 className="text-xl font-semibold text-[var(--neutral-900)]">
{page.timelineTitle || t("history")}
</h2>
<ol className="mt-4 space-y-3">
{page.timeline.map((item) => (
<li key={`${item.year}-${item.text}`} className="flex gap-4">
<span className="w-16 shrink-0 font-semibold text-[var(--brand-700)]">{item.year}</span>
<span className="text-[var(--neutral-700)]">{item.text}</span>
</li>
))}
</ol>
</div>
) : null}
{page.timeline.length || page.tableRows.length ? <RichText value={page.content} /> : null}
{page.people.length ? <PeopleBlock page={page} /> : null}
{page.cards.length ? <CardsBlock page={page} /> : null}
{page.faq.length ? (
<div className="space-y-4">
{page.faq.map((item) => (
<Card key={item.question}>
<h2 className="text-lg font-semibold text-[var(--neutral-900)]">{item.question}</h2>
<p className="mt-2 leading-relaxed text-[var(--neutral-700)]">{item.answer}</p>
</Card>
))}
</div>
) : null}
{page.notice ? <ComingSoon text={page.notice} /> : null}
{page.ctaHref && ctaLabel ? (
page.ctaHint ? (
<p className="text-sm text-[var(--text-muted)]">
{page.ctaHint}{" "}
<PageAnchor href={page.ctaHref} className="font-medium text-[var(--brand-700)] hover:underline">
{ctaLabel}
</PageAnchor>
</p>
) : (
<PageAnchor href={page.ctaHref} className="inline-flex">
{page.ctaHref.startsWith("http") && !page.facts.length ? (
<span className="font-medium text-[var(--brand-700)] hover:underline">{ctaLabel}</span>
) : (
<Button variant="outline">{ctaLabel}</Button>
)}
</PageAnchor>
)
) : null}
{page.sourceUrl ? <SourceNote href={page.sourceUrl} label={t("source")} /> : null}
</div>
</>
);
}