B2B-88: add starter kit structure and elements

This commit is contained in:
devmc-ee
2025-06-08 16:18:30 +03:00
parent 657a36a298
commit e7b25600cb
1280 changed files with 77893 additions and 5688 deletions

View File

@@ -0,0 +1,58 @@
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { ArrowLeft, ArrowRight } from 'lucide-react';
import { Button } from '@kit/ui/button';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
export function BlogPagination(props: {
currentPage: number;
canGoToNextPage: boolean;
canGoToPreviousPage: boolean;
}) {
const navigate = useGoToPage();
return (
<div className={'flex items-center space-x-2'}>
<If condition={props.canGoToPreviousPage}>
<Button
variant={'outline'}
onClick={() => {
navigate(props.currentPage - 1);
}}
>
<ArrowLeft className={'mr-2 h-4'} />
<Trans i18nKey={'marketing:blogPaginationPrevious'} />
</Button>
</If>
<If condition={props.canGoToNextPage}>
<Button
variant={'outline'}
onClick={() => {
navigate(props.currentPage + 1);
}}
>
<Trans i18nKey={'marketing:blogPaginationNext'} />
<ArrowRight className={'ml-2 h-4'} />
</Button>
</If>
</div>
);
}
function useGoToPage() {
const router = useRouter();
const path = usePathname();
return (page: number) => {
const searchParams = new URLSearchParams({
page: page.toString(),
});
router.push(path + '?' + searchParams.toString());
};
}

View File

@@ -0,0 +1,28 @@
import Image from 'next/image';
import { cn } from '@kit/ui/utils';
type Props = {
title: string;
src: string;
preloadImage?: boolean;
className?: string;
};
export function CoverImage({ title, src, preloadImage, className }: Props) {
return (
<Image
className={cn(
'block rounded-xl object-cover duration-250' +
' transition-all hover:opacity-90',
{
className,
},
)}
src={src}
priority={preloadImage}
alt={`Cover Image for ${title}`}
fill
/>
);
}

View File

@@ -0,0 +1,11 @@
import { format, parseISO } from 'date-fns';
type Props = {
dateString: string;
};
export const DateFormatter = ({ dateString }: Props) => {
const date = parseISO(dateString);
return <time dateTime={dateString}>{format(date, 'PP')}</time>;
};

View File

@@ -0,0 +1,9 @@
import React from 'react';
export function DraftPostBadge({ children }: React.PropsWithChildren) {
return (
<span className="dark:text-dark-800 rounded-md bg-yellow-200 px-4 py-2 font-semibold">
{children}
</span>
);
}

View File

@@ -0,0 +1,50 @@
import { Cms } from '@kit/cms';
import { If } from '@kit/ui/if';
import { cn } from '@kit/ui/utils';
import { CoverImage } from './cover-image';
import { DateFormatter } from './date-formatter';
export function PostHeader({ post }: { post: Cms.ContentItem }) {
const { title, publishedAt, description, image } = post;
return (
<div className={'flex flex-1 flex-col'}>
<div className={cn('border-b py-8')}>
<div className={'mx-auto flex max-w-3xl flex-col space-y-4'}>
<h1
className={
'font-heading text-3xl font-semibold tracking-tighter xl:text-5xl dark:text-white'
}
>
{title}
</h1>
<div>
<span className={'text-muted-foreground'}>
<DateFormatter dateString={publishedAt} />
</span>
</div>
<h2
className={'text-muted-foreground text-base xl:text-lg'}
dangerouslySetInnerHTML={{ __html: description ?? '' }}
></h2>
</div>
</div>
<If condition={image}>
{(imageUrl) => (
<div className="relative mx-auto mt-8 flex h-[378px] w-full max-w-3xl justify-center">
<CoverImage
preloadImage
className="rounded-md"
title={title}
src={imageUrl}
/>
</div>
)}
</If>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import Link from 'next/link';
import { Cms } from '@kit/cms';
import { If } from '@kit/ui/if';
import { CoverImage } from '~/(marketing)/blog/_components/cover-image';
import { DateFormatter } from '~/(marketing)/blog/_components/date-formatter';
type Props = {
post: Cms.ContentItem;
preloadImage?: boolean;
imageHeight?: string | number;
};
const DEFAULT_IMAGE_HEIGHT = 250;
export function PostPreview({
post,
preloadImage,
imageHeight,
}: React.PropsWithChildren<Props>) {
const { title, image, publishedAt, description } = post;
const height = imageHeight ?? DEFAULT_IMAGE_HEIGHT;
const slug = `/blog/${post.slug}`;
return (
<div className="transition-shadow-sm flex flex-col gap-y-4 rounded-lg duration-500">
<If condition={image}>
{(imageUrl) => (
<div className="relative mb-2 w-full" style={{ height }}>
<Link href={slug}>
<CoverImage
preloadImage={preloadImage}
title={title}
src={imageUrl}
/>
</Link>
</div>
)}
</If>
<div className={'flex flex-col space-y-4 px-1'}>
<div className={'flex flex-col space-y-2'}>
<h2 className="text-xl leading-snug font-semibold tracking-tight">
<Link href={slug} className="hover:underline">
{title}
</Link>
</h2>
<div className="flex flex-row items-center gap-x-3 text-sm">
<div className="text-muted-foreground">
<DateFormatter dateString={publishedAt} />
</div>
</div>
</div>
<p
className="text-muted-foreground mb-4 text-sm leading-relaxed"
dangerouslySetInnerHTML={{ __html: description ?? '' }}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,24 @@
import type { Cms } from '@kit/cms';
import { ContentRenderer } from '@kit/cms';
import { PostHeader } from './post-header';
export function Post({
post,
content,
}: {
post: Cms.ContentItem;
content: unknown;
}) {
return (
<div>
<PostHeader post={post} />
<div className={'mx-auto flex max-w-3xl flex-col space-y-6 py-8'}>
<article className="markdoc">
<ContentRenderer content={content} />
</article>
</div>
</div>
);
}