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,50 @@
import { PageLink } from '#web-runtime/components/links';
import { RichTextContent } from '#web-runtime/components/rich-text';
import type { ArticlesBlock as ArticlesBlockData } from '#web-runtime/types';
export default function ArticlesBlock({ block }: { block: ArticlesBlockData }) {
const columnClasses = {
1: 'grid-cols-1',
2: 'grid-cols-2 max-[45rem]:grid-cols-1',
3: 'grid-cols-3 max-lg:grid-cols-2 max-[45rem]:grid-cols-1'
}[Math.min(block.items.length, 3) as 1 | 2 | 3];
return (
<section
className="border-b border-[#d6d7cf] py-[clamp(4.5rem,9vw,8rem)] nth-of-type-[even]:bg-[#fffdf8]"
data-page-block={block.type}
>
<div className="mx-auto w-[min(calc(100%-2rem),76rem)] max-[45rem]:w-[min(calc(100%-1.25rem),76rem)]">
{block.title || block.subtitle.content || block.cta ? (
<header className="mb-[clamp(2rem,5vw,4rem)] flex flex-wrap items-end justify-between gap-x-8 gap-y-6">
<div className="max-w-3xl">
{block.title ? (
<h2 className="text-[clamp(2rem,4vw,3.8rem)] leading-[1.04] font-semibold tracking-[-0.055em]">
{block.title}
</h2>
) : null}
<RichTextContent value={block.subtitle} className="mt-4 max-w-160 text-lg" />
</div>
{block.cta ? <PageLink link={block.cta} /> : null}
</header>
) : null}
<div className={`grid gap-4 ${columnClasses}`}>
{block.items.map((item, index) => (
<article
className="flex min-w-0 flex-col items-start gap-4 rounded-[1.25rem] border border-[#d6d7cf] bg-white/50 p-[clamp(1.5rem,3vw,2.25rem)]"
key={`${block.id}-item-${index}`}
>
{item.title ? (
<h3 className="text-[1.35rem] leading-[1.18] font-semibold tracking-[-0.035em]">
{item.title}
</h3>
) : null}
<RichTextContent value={item.content} />
</article>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,32 @@
import { LinkList } from '#web-runtime/components/links';
import { RichTextContent } from '#web-runtime/components/rich-text';
import type { BannerBlock as BannerBlockData } from '#web-runtime/types';
const styleClasses: Record<BannerBlockData['style'], string> = {
info: 'border-[#a9bed9] bg-[#eef3fb]',
success: 'border-[#99d900] bg-[#f0fbd4]',
warning: 'border-[#e3b341] bg-[#fdf3d8]',
error: 'border-[#e0604d] bg-[#fdeae5]'
};
export default function BannerBlock({ block }: { block: BannerBlockData }) {
return (
<section
className="border-b border-[#d6d7cf] py-[clamp(2.5rem,5vw,4rem)] nth-of-type-[even]:bg-[#fffdf8]"
data-page-block={block.type}
>
<div className="mx-auto w-[min(calc(100%-2rem),76rem)] max-[45rem]:w-[min(calc(100%-1.25rem),76rem)]">
<div
className={`flex flex-wrap items-center justify-between gap-x-8 gap-y-5 rounded-[1.25rem] border p-[clamp(1.5rem,3vw,2.25rem)] ${styleClasses[block.style]}`}
data-banner-style={block.style}
>
<RichTextContent
value={block.content}
className="min-w-64 flex-1 basis-96 text-[#172019]"
/>
<LinkList links={block.ctaItems} className="shrink-0" />
</div>
</div>
</section>
);
}
@@ -0,0 +1,47 @@
import { LinkList } from '#web-runtime/components/links';
import { RichTextContent } from '#web-runtime/components/rich-text';
import type { ContentBlock as ContentBlockData } from '#web-runtime/types';
export default function ContentBlock({ block }: { block: ContentBlockData }) {
const columnClasses = {
1: 'grid-cols-1',
2: 'grid-cols-2 max-[45rem]:grid-cols-1',
3: 'grid-cols-3 max-lg:grid-cols-1'
}[Math.min(block.columns.length, 3) as 1 | 2 | 3];
return (
<section
className="border-b border-[#d6d7cf] py-[clamp(4.5rem,9vw,8rem)] nth-of-type-[even]:bg-[#fffdf8]"
data-page-block={block.type}
>
<div className="mx-auto w-[min(calc(100%-2rem),76rem)] max-[45rem]:w-[min(calc(100%-1.25rem),76rem)]">
{block.title || block.subtitle ? (
<header className="mb-[clamp(2rem,5vw,4rem)] max-w-3xl">
{block.title ? (
<h2 className="text-[clamp(2rem,4vw,3.8rem)] leading-[1.04] font-semibold tracking-[-0.055em]">
{block.title}
</h2>
) : null}
{block.subtitle ? (
<p className="mt-4 max-w-160 text-lg leading-[1.6] text-[#5d675f]">
{block.subtitle}
</p>
) : null}
</header>
) : null}
<div className={`grid gap-4 ${columnClasses}`}>
{block.columns.map((column, index) => (
<article
className="flex min-w-0 flex-col items-start gap-6 rounded-[1.25rem] border border-[#d6d7cf] bg-white/50 p-[clamp(1.5rem,3vw,2.25rem)] [&>div:last-child]:mt-auto"
key={`${block.id}-column-${index}`}
>
<RichTextContent value={column.content} />
<LinkList links={column.ctaItems} />
</article>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,41 @@
import type { FaqBlock as FaqBlockData } from '#web-runtime/types';
export default function FaqBlock({ block }: { block: FaqBlockData }) {
return (
<section
className="border-b border-[#d6d7cf] py-[clamp(4.5rem,9vw,8rem)] nth-of-type-[even]:bg-[#fffdf8]"
data-page-block={block.type}
>
<div className="mx-auto grid w-[min(calc(100%-2rem),76rem)] grid-cols-[minmax(15rem,0.65fr)_minmax(0,1fr)] items-start gap-[clamp(2rem,7vw,7rem)] max-[45rem]:w-[min(calc(100%-1.25rem),76rem)] max-[45rem]:grid-cols-1">
{block.title || block.subtitle ? (
<header className="sticky top-8 max-w-3xl max-[45rem]:static">
{block.title ? (
<h2 className="text-[clamp(2rem,4vw,3.8rem)] leading-[1.04] font-semibold tracking-[-0.055em]">
{block.title}
</h2>
) : null}
{block.subtitle ? (
<p className="mt-4 max-w-160 text-lg leading-[1.6] text-[#5d675f]">
{block.subtitle}
</p>
) : null}
</header>
) : null}
<div className="border-t border-[#172019]">
{block.items.map((item, index) => (
<details
className="group border-b border-[#d6d7cf] py-5"
key={`${block.id}-item-${index}`}
>
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 text-[1.05rem] leading-[1.4] font-bold marker:hidden after:shrink-0 after:font-mono after:text-[1.35rem] after:text-[#517000] after:content-['+'] group-open:after:content-['−'] [&::-webkit-details-marker]:hidden">
{item.question}
</summary>
<p className="mt-4 max-w-180 leading-[1.7] text-[#5d675f]">{item.answer}</p>
</details>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,24 @@
import { MediaImage } from '#web-runtime/components/media-image';
import type { MediaBlock as MediaBlockData } from '#web-runtime/types';
export default function MediaBlock({ block }: { block: MediaBlockData }) {
if (!block.media?.url) {
return null;
}
return (
<section
className="border-b border-[#d6d7cf] py-[clamp(4.5rem,9vw,8rem)] nth-of-type-[even]:bg-[#fffdf8]"
data-page-block={block.type}
>
<div className="mx-auto w-[min(calc(100%-2rem),76rem)] max-[45rem]:w-[min(calc(100%-1.25rem),76rem)]">
<figure className="m-0">
<MediaImage
media={block.media}
className="rounded-4xl shadow-[0_1.5rem_4rem_rgb(23_32_25/12%)]"
/>
</figure>
</div>
</section>
);
}
@@ -0,0 +1,36 @@
import { LinkList } from '#web-runtime/components/links';
import { MediaImage } from '#web-runtime/components/media-image';
import { RichTextContent } from '#web-runtime/components/rich-text';
import type { MediaContentBlock as MediaContentBlockData } from '#web-runtime/types';
export default function MediaContentBlock({ block }: { block: MediaContentBlockData }) {
return (
<section
className="border-b border-[#d6d7cf] py-[clamp(4.5rem,9vw,8rem)] nth-of-type-[even]:bg-[#fffdf8]"
data-page-block={block.type}
>
<div className="mx-auto grid w-[min(calc(100%-2rem),76rem)] grid-cols-[minmax(0,1fr)_minmax(0,0.9fr)] items-center gap-[clamp(2.5rem,7vw,6rem)] max-[45rem]:w-[min(calc(100%-1.25rem),76rem)] max-[45rem]:grid-cols-1">
<figure
className={`m-0 empty:hidden ${block.mediaPosition === 'right' ? 'order-2 max-[45rem]:order-0' : ''}`}
>
<MediaImage
media={block.media}
className="aspect-4/3 rounded-4xl shadow-[0_1.5rem_4rem_rgb(23_32_25/12%)]"
/>
</figure>
<div>
{block.title ? (
<header className="mb-6 max-w-3xl">
<h2 className="text-[clamp(2rem,4vw,3.8rem)] leading-[1.04] font-semibold tracking-[-0.055em]">
{block.title}
</h2>
</header>
) : null}
<RichTextContent value={block.content} />
<LinkList links={block.ctaItems} className="mt-8" />
</div>
</div>
</section>
);
}
@@ -0,0 +1,18 @@
import type { PageBlock } from '#web-runtime/types';
export default function UnsupportedBlock({ block }: { block: PageBlock }) {
if (process.env.NODE_ENV !== 'development') {
return null;
}
return (
<section
className="border-b border-[#d6d7cf] bg-[#fff0d7] py-4 font-mono text-sm text-[#754200]"
data-page-block={block.type}
>
<div className="mx-auto w-[min(calc(100%-2rem),76rem)] max-[45rem]:w-[min(calc(100%-1.25rem),76rem)]">
Renderer missing for block type <code>{block.type}</code>.
</div>
</section>
);
}