Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"vueIndentScriptAndStyle": false,
"proseWrap": "preserve",
"insertPragma": false,
"printWidth": 100,
"printWidth": 140,
"requirePragma": false,
"tabWidth": 4,
"useTabs": false,
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,17 @@ update any relevant styles or configurations as needed.

After adding a new component, you will likely need to fix the code style to adhere to our formatting standards. Additionally, ensure the cn
import is correctly handled, as it may need to be adjusted.

# Live Release

This project uses **Docker Hub Automated Builds** for continuous deployment:

1. **Code Push**: When code is pushed to the GitHub repository, Docker Hub automatically detects the changes
2. **Automatic Build**: Docker Hub executes the `hooks/build` script to build the Docker image
3. **Environment Selection**: The build script automatically selects production or development environment variables based on the git branch:
- `main` branch → Production environment variables (`latest` tag)
- `dev` branch → Development environment variables (`latest-test` tag)
4. **Image Tagging**: The built image is tagged with the git commit hash and branch-specific tags:
- `main` → `seebruecke/bside-website:a1b2c3d` + `seebruecke/bside-website:latest`
- `dev` → `seebruecke/bside-website:b2c3d4e` + `seebruecke/bside-website:latest-test`
5. **Registry Push**: The image is automatically pushed to Docker Hub
2 changes: 1 addition & 1 deletion components/blocks/buttonBlock/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import isEmptyString from '@/lib/common/helper/isEmptyString';
import { toKebabCase } from '@/lib/common/toKebabCase';

interface Props {
title: string | undefined;
title?: string | null;
text: string;
href: string;
target?: '_blank' | '_self';
Expand Down
2 changes: 1 addition & 1 deletion components/blocks/callToActionBlock/CallToAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import isEmptyString from '@/lib/common/helper/isEmptyString';
import { toKebabCase } from '@/lib/common/toKebabCase';

interface Props {
title: string | undefined;
title?: string | null;
text: string;
href: string;
withArrows?: boolean;
Expand Down
28 changes: 22 additions & 6 deletions components/blocks/mediaBlock/MediaBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import clsx from 'clsx';
import Image from 'next/image';
import type { ReactElement } from 'react';
import { isNil } from 'lodash-es';
import ContentWrapper from '@/components/layout/ContentWrapper';
import isEmptyString from '@/lib/common/helper/isEmptyString';
import isEmptyNumber from '@/lib/common/helper/isEmptyNumber';
import type { MediaBlockProps } from '@/types/payload/Blocks';

const WideMediaBlock = (
Expand All @@ -28,20 +30,34 @@ const WideMediaBlock = (

const MediaBlock = ({
media,
caption = '',
caption,
size,
effects,
}: MediaBlockProps): ReactElement | null => {
if (typeof media === 'string') {
console.warn('Unexpectedly media is just a string, this is not supported.');
return null;
}

if (media.url === undefined || media.url === null || isEmptyString(media.url)) {
if (isEmptyString(media.url)) {
console.warn('Unexpectedly media URL is not set, something went wrong here.');
return null;
}

if (size === 'wide' && media.sizes?.wide?.url !== undefined) {
return WideMediaBlock(media.sizes.wide.url!, effects ?? []);
if (size === 'wide') {
if (isNil(media.sizes?.wide?.url)) {
console.warn('Unexpectedly wide version of image is not available.');
return null;
}

return WideMediaBlock(media.sizes.wide.url, effects ?? []);
}

let [width, height] = [media.width, media.height];

if (isEmptyNumber(width) || isEmptyNumber(height)) {
console.warn('Unexpectedly size of image is not set, using somewhat arbitrary fallback.');
[width, height] = [100, 100];
}

// ToDo: Implement the square version of the image.
Expand All @@ -52,8 +68,8 @@ const MediaBlock = ({
<Image
src={media.url}
alt={media.alt}
width={media.width!}
height={media.height!}
width={width}
height={height}
className={clsx(
'mx-auto',
(effects?.includes('blur') ?? false) && 'blur-[2px]',
Expand Down
2 changes: 1 addition & 1 deletion components/blocks/mediaContent/MediaContentBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const MediaContentBlock = ({
<MediaContentOverlay
media={media}
richText={richText}
headlineText={headline}
headlineText={headline ?? undefined}
effects={effects ?? []}
/>
);
Expand Down
17 changes: 10 additions & 7 deletions components/blocks/reusableLayout/ReusableBlocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const ReusableBlocks = ({
case 'callToAction':
return (
<CallToActionBlock
title={layoutElement.title!}
title={layoutElement.title}
text={layoutElement.text}
href={layoutElement.href}
/>
Expand All @@ -40,7 +40,7 @@ const ReusableBlocks = ({
case 'content':
return (
<ContentBlock
columns={layoutElement.columns ?? []}
columns={layoutElement.columns}
backgroundColor={layoutElement.backgroundColor}
backgroundWidth={layoutElement.backgroundWidth}
/>
Expand All @@ -50,9 +50,9 @@ const ReusableBlocks = ({
return (
<MediaBlock
media={layoutElement.media}
size={layoutElement.size!}
caption={layoutElement.caption!}
effects={layoutElement.effects!}
size={layoutElement.size}
caption={layoutElement.caption}
effects={layoutElement.effects}
/>
);

Expand All @@ -62,9 +62,9 @@ const ReusableBlocks = ({
media={layoutElement.media}
richText={layoutElement.richText}
alignment={layoutElement.alignment}
headline={layoutElement.headline!}
headline={layoutElement.headline}
backgroundColor={layoutElement.backgroundColor}
effects={layoutElement.effects!}
effects={layoutElement.effects}
/>
);

Expand Down Expand Up @@ -119,6 +119,9 @@ const ReusableBlocks = ({
return <SliderBlock imageSlides={layoutElement.imageSlides ?? []} />;

default:
// @ts-expect-error | This should not happen, just precaution.
console.warn(`Received unexpected block type ${layoutElement.blockType}`);

return null;
}
};
Expand Down
59 changes: 37 additions & 22 deletions components/blocks/richTextBlock/serializeRichTextToHtml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ import type { SlateChildren } from '@/types/payload/Blocks';
import type { Media as MediaType } from '@/types/payload/payload-types';
import InlineButton from '@blocks/buttonBlock/InlineButton';
import HeadlineTag from 'components/blocks/headlineBlock/HeadlineTag';
import isNotEmptyString from '@/lib/common/helper/isNotEmptyString';
import isEmptyString from '@/lib/common/helper/isEmptyString';

interface LinkSlateChild {
type: 'link';
linkType?: 'custom' | 'internal';
url: string | null;
newTab: boolean;
fields?: { appearance: 'link' | 'button' };
children: Array<{ text: string }>;
}

export interface RichTextUploadNodeType {
value?: MediaType;
Expand Down Expand Up @@ -95,48 +106,52 @@ const serializeMedia = (node: Record<string, unknown>): ReactElement | null => {
);
};

const serializeLink = (
nodeChildren: SlateChildren,
node: Record<string, unknown>,
index: number,
): ReactElement => {
// @ts-expect-error Needs to be typed.
const serializeLink = (node: LinkSlateChild, index: number): ReactElement => {
if (node.linkType === 'internal') {
// TODO: Add support for internal links - or prevent option in Payload. Also it seems to cause errors, when there's
// an internal link to a doc that's not public, e.g. a user.
console.warn('Link type "internal" is currently not supported!');
return <></>
}

const linkText = node.children[0]?.text;

if (isEmptyString(node.url) || isEmptyString(linkText)) {
// Might accidentally happen while pasting copied text into the rich text editor, or by some other mistake.
console.warn('Unexpectedly link URL or text is empty');
return <></>;
}

if (node.fields?.appearance === 'button') {
return (
<InlineButton
key={index}
title=""
text={nodeChildren[0] !== undefined ? (nodeChildren[0].text as string) : ''}
// @ts-expect-error Need to find more type safe solution in future
title={linkText}
text={linkText}
href={escapeHTML(node.url)}
target={node.newTab === true ? '_blank' : '_self'}
target={node.newTab ? '_blank' : '_self'}
/>
);
}

if ((node.url as string).startsWith('mailto:')) {
let mail = (node.url as string).substring(7);
if (isNotEmptyString(node.url) && node.url.startsWith('mailto:')) {
let mail = node.url.substring(7);
mail = mail.startsWith('//') ? mail.substring(2) : mail;
mail = mail.replace('.spam', '.ms');

return (
<Obfuscate
email={mail}
key={`mail-${mail}`}
className="italic underline underline-offset-4 hover:text-orange-500 sm:text-lg"
/>
<Obfuscate email={mail} key={`mail-${mail}`} className="italic underline underline-offset-4 hover:text-orange-500 sm:text-lg" />
);
}

return (
<Link
key={index}
// @ts-expect-error Need to find more type safe solution in future
href={escapeHTML(node.url)}
target={node.newTab === true ? '_blank' : '_self'}
target={node.newTab ? '_blank' : '_self'}
className="italic underline underline-offset-4 hover:text-orange-500 sm:text-lg"
>
{}
{serializeRichTextToHtml(nodeChildren)}
{linkText}
</Link>
);
};
Expand Down Expand Up @@ -164,7 +179,7 @@ const serializeRichTextToHtml = (children: SlateChildren): Array<ReactElement |
);

case 'link':
return serializeLink(nodeChildren, node, index);
return serializeLink(node as unknown as LinkSlateChild, index);

case 'ul':
return (
Expand Down
26 changes: 10 additions & 16 deletions components/blocks/textBlock/ColumnsContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import HalfColumnsContent from '@blocks/textBlock/HalfColumnsContent';
import OneThirdColumnsContent from '@blocks/textBlock/OneThirdColumnsContent';
import TwoThirdsColumnsContent from '@blocks/textBlock/TwoThirdsColumnsContent';

const ColumnsContent = ({
columns,
}: {
interface Props {
columns: Array<ContentColumnProps>;
}): ReactElement | null => {
}

const ColumnsContent = ({ columns }: Props): ReactElement | null => {
const [firstColumn, secondColumn, thirdColumn] = columns;

if (firstColumn === undefined) {
Expand All @@ -26,21 +26,15 @@ const ColumnsContent = ({
return <HalfColumnsContent firstColumn={firstColumn} secondColumn={secondColumn} />;

case 'twoThirds':
return (
<TwoThirdsColumnsContent firstColumn={firstColumn} secondColumn={secondColumn} />
);
return <TwoThirdsColumnsContent firstColumn={firstColumn} secondColumn={secondColumn} />;

case 'oneThird':
return (
<OneThirdColumnsContent
firstColumn={firstColumn}
secondColumn={secondColumn}
thirdColumn={thirdColumn}
/>
);
}
return <OneThirdColumnsContent firstColumn={firstColumn} secondColumn={secondColumn} thirdColumn={thirdColumn} />;

return <div />;
default:
console.error(`Received unexpected width of first column: ${firstColumn.width}`)
return null;
}
};

export default ColumnsContent;
6 changes: 0 additions & 6 deletions components/blocks/textBlock/ContentBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ const ContentBlock = ({
backgroundWidth,
columns,
}: ContentProps): ReactElement | null => {
if (columns === undefined || columns.length === 0) {
console.warn('Columns must be set');

return null;
}

if (backgroundWidth === 'full' && backgroundColor === 'black') {
return (
<div className="grow">
Expand Down
53 changes: 15 additions & 38 deletions components/blocks/textBlock/OneThirdColumnsContent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { ReactElement } from 'react';
import type { ContentColumnProps } from '@/types/payload/Blocks';
import FullColumnContent from '@blocks/textBlock/FullColumnContent';
import TwoThirdsColumnsContent from '@blocks/textBlock/TwoThirdsColumnsContent';
import RichText from 'components/blocks/richTextBlock/RichText';

interface Props {
Expand All @@ -10,48 +9,26 @@ interface Props {
thirdColumn?: ContentColumnProps;
}

const OneThirdColumnsContent = ({
firstColumn,
secondColumn,
thirdColumn,
}: Props): ReactElement | null => {
if (secondColumn === undefined) {
console.warn(
'Falling back to full column instead of oneThird since second column is missing',
);
const OneThirdColumnsContent = ({ firstColumn, secondColumn, thirdColumn }: Props): ReactElement => {
if (secondColumn === undefined || thirdColumn === undefined) {
console.warn('Falling back to full since second or third column is missing');

return <FullColumnContent firstColumn={firstColumn} />;
}

// Three Columns
if (secondColumn.width === 'oneThird') {
if (thirdColumn === undefined) {
console.warn(
'Falling back to two columns instead of oneThird since third column is missing',
);

return (
<TwoThirdsColumnsContent firstColumn={firstColumn} secondColumn={secondColumn} />
);
}

return (
<div className="grid grid-cols-1 md:grid-cols-3 md:gap-4">
<div>
<RichText content={firstColumn.richText} />
</div>
<div className="pt-4 md:py-0">
<RichText content={secondColumn.richText} />
</div>
<div className="pt-4 md:py-0">
<RichText content={thirdColumn.richText} />
</div>
return (
<div className="grid grid-cols-1 md:grid-cols-3 md:gap-4">
<div>
<RichText content={firstColumn.richText} />
</div>
);
}

// Two Columns.
return <TwoThirdsColumnsContent firstColumn={firstColumn} secondColumn={secondColumn} />;
<div className="pt-4 md:py-0">
<RichText content={secondColumn.richText} />
</div>
<div className="pt-4 md:py-0">
<RichText content={thirdColumn.richText} />
</div>
</div>
);
};

export default OneThirdColumnsContent;
Loading