TASKS-1823 WebManager module

This commit is contained in:
Igor Martyniuk
2026-08-28 08:35:58 +00:00
parent fdb2cf3837
commit c93d8c025b
654 changed files with 26232 additions and 6075 deletions
@@ -0,0 +1,196 @@
import { getTranslations } from 'next-intl/server';
import { cn } from '@crux-kit/utils';
import { getWebRuntimeAPI } from '#web-runtime/api/web-runtime-api';
import { MediaImage } from '#web-runtime/components/media-image';
import type { PageDocument, PageSearchParams, PageTeaser, PageTeaserPagination } from '#web-runtime/types';
import { toPublicHref } from '#i18n/content-href';
import { Button } from '#shared/ui/Button';
import { pagerPages, requestedPage } from './blog-pager';
import Breadcrumbs from './Breadcrumbs';
import PageHero from './PageHero';
const POSTS_PER_PAGE = 12;
function PostCard({
post,
locale,
readMoreLabel
}: {
post: PageTeaser;
locale: string;
readMoreLabel: string;
}) {
const href = toPublicHref(`/${locale}/${post.path}`);
const publishedAt = post.publishedAt ? new Date(post.publishedAt) : null;
return (
<article className="flex min-w-0 flex-col rounded-[1.25rem] border border-[#d6d7cf] bg-white/50 transition duration-150 hover:-translate-y-0.5 hover:border-[#172019]/35 motion-reduce:transform-none motion-reduce:transition-none">
<a
href={href}
className="flex flex-1 flex-col gap-4 p-[clamp(1.25rem,2.5vw,1.75rem)] focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-[#99d900]"
>
{post.media ? (
<MediaImage media={post.media} className="aspect-8/5 rounded-xl" />
) : (
<div className="aspect-8/5 rounded-xl bg-[#e8e6dc]" aria-hidden="true" />
)}
<div className="flex flex-1 flex-col items-start gap-2">
{publishedAt && !Number.isNaN(publishedAt.getTime()) ? (
<time
dateTime={post.publishedAt ?? undefined}
className="font-mono text-[0.7rem] font-bold tracking-[0.08em] text-[#5d675f] uppercase"
>
{new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(publishedAt)}
</time>
) : null}
<h2 className="text-[1.35rem] leading-[1.18] font-semibold tracking-[-0.035em]">
{post.title}
</h2>
{post.description ? (
<p className="line-clamp-3 text-sm leading-relaxed text-[#5d675f]">
{post.description}
</p>
) : null}
{/* The whole card is the link; this is its visual call to action. */}
<Button asChild variant="inline" size="sm" className="mt-auto pt-2">
<span aria-hidden="true">{readMoreLabel} →</span>
</Button>
</div>
</a>
</article>
);
}
function Pager({
pagination,
basePath,
labels
}: {
pagination: PageTeaserPagination;
basePath: string;
labels: { pagination: string; previous: string; next: string };
}) {
if (pagination.lastPage <= 1) {
return null;
}
const pageHref = (page: number) => (page <= 1 ? basePath : `${basePath}?page=${page}`);
const controlClassName = 'size-11 p-0';
const pages = pagerPages(pagination.page, pagination.lastPage);
return (
<nav
className="mt-[clamp(2.5rem,5vw,4rem)] flex flex-wrap items-center justify-center gap-2"
aria-label={labels.pagination}
>
{pagination.page > 1 ? (
<Button asChild variant="outline" className={controlClassName}>
<a href={pageHref(pagination.page - 1)} rel="prev" aria-label={labels.previous}>
←
</a>
</Button>
) : (
<Button variant="outline" className={controlClassName} disabled aria-hidden="true">
←
</Button>
)}
{pages.map((page, index) => {
const previousPage = pages[index - 1];
return (
<span className="flex items-center gap-2" key={page}>
{previousPage !== undefined && page - previousPage > 1 ? (
<span className="text-[#5d675f]" aria-hidden="true">
…
</span>
) : null}
{page === pagination.page ? (
<Button
asChild
variant="primary"
className={cn(controlClassName, 'pointer-events-none')}
>
<span aria-current="page">{page}</span>
</Button>
) : (
<Button asChild variant="outline" className={controlClassName}>
<a href={pageHref(page)}>{page}</a>
</Button>
)}
</span>
);
})}
{pagination.page < pagination.lastPage ? (
<Button asChild variant="outline" className={controlClassName}>
<a href={pageHref(pagination.page + 1)} rel="next" aria-label={labels.next}>
→
</a>
</Button>
) : (
<Button variant="outline" className={controlClassName} disabled aria-hidden="true">
→
</Button>
)}
</nav>
);
}
export default async function BlogPageLayout({
page,
searchParams
}: {
page: PageDocument;
searchParams?: PageSearchParams;
}) {
const t = await getTranslations('Pages.BlogPage');
const { items: posts, pagination } = await getWebRuntimeAPI().listBlogPosts({
locale: page.locale,
page: requestedPage(searchParams),
perPage: POSTS_PER_PAGE
});
return (
<main className="flex-1" data-page-layout={page.layout}>
<Breadcrumbs items={page.breadcrumbs} />
<PageHero page={page} />
<section
className="border-b border-[#d6d7cf] py-[clamp(4.5rem,9vw,8rem)]"
data-page-block="blog-posts"
>
<div className="mx-auto w-[min(calc(100%-2rem),76rem)] max-[45rem]:w-[min(calc(100%-1.25rem),76rem)]">
{posts.length === 0 ? (
<p className="text-lg text-[#5d675f]">{t('empty')}</p>
) : (
<div className="grid grid-cols-3 gap-4 max-lg:grid-cols-2 max-[45rem]:grid-cols-1">
{posts.map((post) => (
<PostCard
key={post.id}
post={post}
locale={page.locale}
readMoreLabel={t('readMore')}
/>
))}
</div>
)}
<Pager
pagination={pagination}
basePath={toPublicHref(`/${page.locale}/${page.path}`)}
labels={{
pagination: t('pagination'),
previous: t('previous'),
next: t('next')
}}
/>
</div>
</section>
</main>
);
}