TASKS-1823 WebManager module
This commit is contained in:
@@ -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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user