project setup with core files including configuration, package management, and basic structure. Added .gitignore, README, and various TypeScript types for CMS components. Implemented initial components and layouts for the application.

This commit is contained in:
Peter Meier
2025-12-13 23:26:13 +01:00
parent ea288a5bbc
commit b1a556dc6d
167 changed files with 19057 additions and 131 deletions

View File

@@ -0,0 +1,235 @@
import type { Page as CmsPage } from "../types/cms/Page";
import type { HTMLSkeleton } from "../types/cms/Html";
import type { MarkdownSkeleton } from "../types/cms/Markdown";
import type { ComponentIframeSkeleton } from "../types/cms/Iframe";
import type { ImageGallerySkeleton } from "../types/cms/ImageGallery";
import type { ComponentImageSkeleton } from "../types/cms/Image";
import type { QuoteSkeleton } from "../types/cms/Quote";
import type { ComponentYoutubeVideoSkeleton } from "../types/cms/YoutubeVideo";
import type { ComponentHeadlineSkeleton } from "../types/cms/Headline";
import type { Page, ContentItem, ContentRow } from "../types/page";
import { ContentType } from "../types/cms/ContentType.enum";
type ContentEntry =
| HTMLSkeleton
| MarkdownSkeleton
| ComponentIframeSkeleton
| ImageGallerySkeleton
| ComponentImageSkeleton
| QuoteSkeleton
| ComponentYoutubeVideoSkeleton
| ComponentHeadlineSkeleton;
/**
* Mapper für Page-Transformationen
* Konvertiert CMS-Typen zu unseren Domain-Typen
*/
export class PageMapper {
/**
* Strategy Pattern: Map mit Mapper-Funktionen für jeden Content-Type
*/
private static contentMappers = new Map<
ContentType,
(entry: ContentEntry) => ContentItem | null
>([
[
ContentType.html,
(entry) => {
const htmlEntry = entry as HTMLSkeleton;
return {
type: "html",
name: htmlEntry.fields.id || "",
html: htmlEntry.fields.html,
layout: htmlEntry.fields.layout,
};
},
],
[
ContentType.markdown,
(entry) => {
const markdownEntry = entry as MarkdownSkeleton;
return {
type: "markdown",
name: markdownEntry.fields.name,
content: markdownEntry.fields.content,
layout: markdownEntry.fields.layout,
alignment: markdownEntry.fields.alignment,
};
},
],
[
ContentType.iframe,
(entry) => {
const iframeEntry = entry as ComponentIframeSkeleton;
return {
type: "iframe",
name: iframeEntry.fields.name,
content: iframeEntry.fields.content,
iframe: iframeEntry.fields.iframe,
overlayImageUrl: iframeEntry.fields.overlayImage?.fields.file.url,
layout: iframeEntry.fields.layout,
};
},
],
[
ContentType.imgGallery,
(entry) => {
const galleryEntry = entry as ImageGallerySkeleton;
return {
type: "imageGallery",
name: galleryEntry.fields.name,
images: galleryEntry.fields.images.map((img) => ({
url: img.fields.file.url,
title: img.fields.title,
description: img.fields.description,
})),
description: galleryEntry.fields.description,
layout: galleryEntry.fields.layout,
};
},
],
[
ContentType.image,
(entry) => {
const imageEntry = entry as ComponentImageSkeleton;
return {
type: "image",
name: imageEntry.fields.name,
imageUrl: imageEntry.fields.image.fields.file.url,
caption: imageEntry.fields.caption,
maxWidth: imageEntry.fields.maxWidth,
aspectRatio: imageEntry.fields.aspectRatio,
layout: imageEntry.fields.layout,
};
},
],
[
ContentType.quote,
(entry) => {
const quoteEntry = entry as QuoteSkeleton;
return {
type: "quote",
quote: quoteEntry.fields.quote,
author: quoteEntry.fields.author,
variant: quoteEntry.fields.variant,
layout: quoteEntry.fields.layout,
};
},
],
[
ContentType.youtubeVideo,
(entry) => {
const videoEntry = entry as ComponentYoutubeVideoSkeleton;
return {
type: "youtubeVideo",
id: videoEntry.fields.id,
youtubeId: videoEntry.fields.youtubeId,
params: videoEntry.fields.params,
title: videoEntry.fields.title,
description: videoEntry.fields.description,
layout: videoEntry.fields.layout,
};
},
],
[
ContentType.headline,
(entry) => {
const headlineEntry = entry as ComponentHeadlineSkeleton;
return {
type: "headline",
internal: headlineEntry.fields.internal,
text: headlineEntry.fields.text,
tag: headlineEntry.fields.tag,
align: headlineEntry.fields.align,
layout: headlineEntry.fields.layout,
};
},
],
]);
/**
* Mappt ein Contentful Content-Item zu unserem ContentItem
* Verwendet Strategy Pattern für wartbaren Code
*/
private static mapContentItem(entry: ContentEntry): ContentItem | null {
if (!entry.contentTypeId || !entry.fields) {
return null;
}
const mapper = this.contentMappers.get(entry.contentTypeId);
if (!mapper) {
return null;
}
return mapper(entry);
}
/**
* Mappt eine CMS Content-Row zu unserer ContentRow
*/
private static mapContentRow(
content: ContentEntry[],
justifyContent: string,
alignItems: string
): ContentRow | undefined {
if (!content || content.length === 0) {
return undefined;
}
const mappedContent = content
.map((entry) => this.mapContentItem(entry))
.filter((item): item is ContentItem => item !== null);
if (mappedContent.length === 0) {
return undefined;
}
return {
justifyContent: justifyContent as ContentRow["justifyContent"],
alignItems: alignItems as ContentRow["alignItems"],
content: mappedContent,
};
}
static fromCms(cmsPage: CmsPage): Page {
return {
slug: cmsPage.slug,
name: cmsPage.name,
linkName: cmsPage.linkName,
headline: cmsPage.headline,
subheadline: cmsPage.subheadline,
seoTitle: cmsPage.seoTitle,
seoMetaRobots: cmsPage.seoMetaRobots,
seoDescription: cmsPage.seoDescription,
topFullwidthBanner: cmsPage.topFullwidthBanner
? {
name: cmsPage.topFullwidthBanner.fields.name,
variant: cmsPage.topFullwidthBanner.fields.variant,
headline: cmsPage.topFullwidthBanner.fields.headline,
subheadline: cmsPage.topFullwidthBanner.fields.subheadline,
text: cmsPage.topFullwidthBanner.fields.text,
imageUrl: cmsPage.topFullwidthBanner.fields.img.fields.file.url,
}
: undefined,
row1: this.mapContentRow(
cmsPage.row1Content,
cmsPage.row1JustifyContent,
cmsPage.row1AlignItems
),
row2: this.mapContentRow(
cmsPage.row2Content,
cmsPage.row2JustifyContent,
cmsPage.row2AlignItems
),
row3: this.mapContentRow(
cmsPage.row3Content,
cmsPage.row3JustifyContent,
cmsPage.row3AlignItems
),
};
}
static fromCmsArray(cmsPages: CmsPage[]): Page[] {
return cmsPages.map((page) => this.fromCms(page));
}
}