Add initial project files

This commit is contained in:
Igor Martyniuk
2026-09-01 13:03:41 +03:00
parent 22e111bb44
commit 74cb5b6176
1841 changed files with 242819 additions and 30 deletions
+66
View File
@@ -0,0 +1,66 @@
import { cn } from '@crux-kit/utils';
import { toPublicHref } from '#i18n/content-href';
import { buttonVariants } from '#shared/ui/Button';
import type { NavigationLink } from '../types';
export function safeHref(link: NavigationLink): string | null {
const href = link.href || link.externalUrl;
if (!href) {
return null;
}
if (href.startsWith('/')) {
return toPublicHref(href);
}
if (href.startsWith('#')) {
return href;
}
try {
const url = new URL(href);
return ['http:', 'https:', 'mailto:', 'tel:'].includes(url.protocol) ? href : null;
} catch {
return null;
}
}
export function PageLink({ link, className = '' }: { link: NavigationLink; className?: string }) {
const href = safeHref(link);
const classes = cn(buttonVariants({ variant: link.variant }), className);
if (!href) {
return <span className={cn(classes, 'cursor-not-allowed opacity-45')}>{link.label}</span>;
}
return (
<a
href={href}
className={classes}
target={link.openInNewTab ? '_blank' : undefined}
rel={link.openInNewTab ? 'noreferrer noopener' : undefined}
>
{link.label}
</a>
);
}
export function LinkList({
links,
className = ''
}: {
links: NavigationLink[];
className?: string;
}) {
if (links.length === 0) {
return null;
}
return (
<div className={cn('flex flex-wrap items-center gap-3', className)}>
{links.map((link, index) => (
<PageLink key={`${link.label}-${index}`} link={link} />
))}
</div>
);
}
@@ -0,0 +1,29 @@
import type { Media } from '../types';
export function MediaImage({
media,
eager = false,
className = ''
}: {
media?: Media;
eager?: boolean;
className?: string;
}) {
if (!media?.url) {
return null;
}
return (
// Media origins are CMS-managed and intentionally unrestricted, so a native image is used here.
// eslint-disable-next-line @next/next/no-img-element
<img
className={`block h-auto w-full object-cover ${className}`}
src={media.url}
alt={media.alt}
width={media.width}
height={media.height}
loading={eager ? 'eager' : 'lazy'}
fetchPriority={eager ? 'high' : 'auto'}
/>
);
}
@@ -0,0 +1,20 @@
import type { RichText } from '../types';
export function RichTextContent({
value,
className = ''
}: {
value?: RichText;
className?: string;
}) {
if (!value?.content) {
return null;
}
return (
<div
className={`text-base leading-7 text-[#5d675f] *:first:mt-0 *:last:mb-0 [&_a]:font-medium [&_a]:underline [&_a]:decoration-[#99d900] [&_a]:decoration-2 [&_a]:underline-offset-4 [&_h2]:mt-[1.25em] [&_h2]:mb-[0.5em] [&_h2]:font-sans [&_h2]:leading-[1.18] [&_h2]:font-semibold [&_h2]:tracking-[-0.035em] [&_h2]:text-[#172019] [&_h3]:mt-[1.25em] [&_h3]:mb-[0.5em] [&_h3]:font-sans [&_h3]:text-[1.35rem] [&_h3]:leading-[1.18] [&_h3]:font-semibold [&_h3]:tracking-[-0.035em] [&_h3]:text-[#172019] [&_h4]:mt-[1.25em] [&_h4]:mb-[0.5em] [&_h4]:font-sans [&_h4]:leading-[1.18] [&_h4]:font-semibold [&_h4]:tracking-[-0.035em] [&_h4]:text-[#172019] [&_li+li]:mt-1.5 [&_ol]:mb-[1em] [&_ol]:list-decimal [&_ol]:pl-5 [&_p]:mb-[1em] [&_ul]:mb-[1em] [&_ul]:list-disc [&_ul]:pl-5 ${className}`}
dangerouslySetInnerHTML={{ __html: value.content }}
/>
);
}