diff --git a/src/lib/assets/images/profile_picture.png b/src/lib/assets/images/profile_picture.png deleted file mode 100644 index 2f315da..0000000 Binary files a/src/lib/assets/images/profile_picture.png and /dev/null differ diff --git a/src/lib/components/Profile.svelte b/src/lib/components/Profile.svelte index aaf10d7..7bb09fc 100644 --- a/src/lib/components/Profile.svelte +++ b/src/lib/components/Profile.svelte @@ -1,14 +1,13 @@
- +

{name}

{title}

diff --git a/src/lib/components/cms/CmsComponent.svelte b/src/lib/components/cms/CmsComponent.svelte new file mode 100644 index 0000000..b546925 --- /dev/null +++ b/src/lib/components/cms/CmsComponent.svelte @@ -0,0 +1,16 @@ + + +{#if component} + {#await component then { default: Component }} + + {/await} +{/if} diff --git a/src/lib/mapping/strapiMapping.svelte.ts b/src/lib/mapping/strapiMapping.svelte.ts new file mode 100644 index 0000000..dda2a35 --- /dev/null +++ b/src/lib/mapping/strapiMapping.svelte.ts @@ -0,0 +1,131 @@ +import type { + StrapiComponentUnion, + StrapiContactComponent, + StrapiFooterResponse, + StrapiHomepageResponse, + StrapiProfileComponent, + StrapiProjectComponent, + StrapiSkillComponent +} from '$lib/types/strapi'; +import type { + Contact, + Footer, + Header, + HeaderLink, + Profile, + Projects, + Skills +} from '$lib/types/data'; +import type { ComponentKeys } from '$lib/types/strapi'; +import type { Component } from 'svelte'; +import { PUBLIC_STRAPI_URL } from '$env/static/public'; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +export const cmsComponentMapping: Record }>> = { + 'shared.profile': import('$lib/components/Profile.svelte'), + 'shared.project': import('$lib/components/Projects.svelte'), + 'shared.skill': import('$lib/components/Skills.svelte'), + 'shared.contact': import('$lib/components/Contact.svelte') +}; + +export const mapHeaderData = (data: StrapiHomepageResponse): Header => { + const links: HeaderLink[] = data.data.components.map((component) => { + if (component.__component === 'shared.profile') { + return { + label: "Profile", + href: `#${component.profile.anchor_id}` + }; + } else if (component.__component === 'shared.project') { + return { + label: component.project.title, + href: `#${component.project.anchor_id}` + }; + } else if (component.__component === 'shared.skill') { + return { + label: component.skill.title, + href: `#${component.skill.anchor_id}` + }; + } + + return { + label: component.contact.title, + href: `#${component.contact.anchor_id}` + }; + }); + + return { links }; +}; + +export const mapFooterData = (data: StrapiFooterResponse): Footer => { + return { + copyright: data.data.copyright, + links: { + git: data.data.git, + linkedin: data.data.linkedin + } + }; +}; + +const mapProfileData = (data: StrapiProfileComponent): Profile => { + return { + name: data.profile.name, + title: data.profile.title, + anchorId: data.profile.anchor_id, + text: data.profile.description, + imageUrl: PUBLIC_STRAPI_URL + data.profile.image.formats.small.url + }; +}; + +const mapProjectData = (data: StrapiProjectComponent): Projects => { + return { + title: data.project.title, + anchorId: data.project.anchor_id, + entries: data.project.entries.map((entry) => ({ + title: entry.front_title, + description: entry.front_description, + taskHeadline: entry.back_title, + tasks: entry.back_description, + toolsHeadline: entry.back_title_second, + tools: entry.back_content_list + })) + }; +}; + +const mapSkillData = (data: StrapiSkillComponent): Skills => { + return { + anchorId: data.skill.anchor_id, + subtitle: data.skill.subtitle, + title: data.skill.title, + entries: data.skill.entries.map((entry) => ({ + title: entry.title, + icon: entry.icon, + items: entry.content + })) + }; +}; + +const mapContactData = (data: StrapiContactComponent): Contact => { + return { + anchorId: data.contact.anchor_id, + subtitle: data.contact.subtitle, + title: data.contact.title, + form: { + title: data.contact.form_title, + subjectPlaceholder: data.contact.subject_placeholder, + messagePlaceholder: data.contact.message_placeholder, + submit: data.contact.cta_text + } + }; +}; + +export const mapComponentData = (data: StrapiComponentUnion) => { + if (data.__component === 'shared.profile') { + return mapProfileData(data); + } else if (data.__component === 'shared.project') { + return mapProjectData(data); + } else if (data.__component === 'shared.skill') { + return mapSkillData(data); + } + + return mapContactData(data); +}; diff --git a/src/lib/types/data.ts b/src/lib/types/data.ts index 23be4a2..f5aa20e 100644 --- a/src/lib/types/data.ts +++ b/src/lib/types/data.ts @@ -34,6 +34,7 @@ export type Profile = { title: string; anchorId: string; text: string; + imageUrl: string; }; export type Projects = { diff --git a/src/lib/types/strapi.ts b/src/lib/types/strapi.ts new file mode 100644 index 0000000..fe31fcf --- /dev/null +++ b/src/lib/types/strapi.ts @@ -0,0 +1,135 @@ +export type StrapiSharedMetaData = { + id: number; + documentId: string; + createdAt: string; + updatedAt: string; + publishedAt: string; +}; + +export type StrapiImageFormat = { + ext: string; + url: string; + hash: string; + mime: string; + name: string; + path: string; + size: number; + width: number; + height: number; + sizeInBytes: number; +}; + +export type StrapiImage = { + alternativeText: string; + caption: string; + focalPoint: object; + formats: { + small: StrapiImageFormat; + medium: StrapiImageFormat; + thumbnail: StrapiImageFormat; + }; + previewUrl: string; + provider: string; + provider_metadata: object; +} & StrapiSharedMetaData & + Omit; + +export type StrapiHomepageResponse = { + data: StrapiHomepage; + meta: object; +}; + +export type StrapiHomepage = { + title: string; + components: StrapiComponentUnion[]; +} & StrapiSharedMetaData; + +export interface StrapiProfileComponent { + __component: 'shared.profile'; + id: number; + profile: StrapiProfile; +} + +export interface StrapiProjectComponent { + __component: 'shared.project'; + id: number; + project: StrapiProject; +} + +export interface StrapiSkillComponent { + __component: 'shared.skill'; + id: number; + skill: StrapiSkill; +} + +export interface StrapiContactComponent { + __component: 'shared.contact'; + id: number; + contact: StrapiContact; +} + +export type StrapiComponentUnion = + | StrapiProfileComponent + | StrapiProjectComponent + | StrapiSkillComponent + | StrapiContactComponent; + +export type ComponentKeys = StrapiComponentUnion['__component']; + +export type StrapiProfile = { + name: string; + title: string; + description: string; + anchor_id: string; + image: StrapiImage; +} & StrapiSharedMetaData; + +export type StrapiProject = { + title: string; + subtitle: string; + anchor_id: string; + entries: StrapiProjectEntry[]; +} & StrapiSharedMetaData; + +export type StrapiProjectEntry = { + front_title: string; + front_description: string; + back_title: string; + back_description: string; + back_title_second: string; + back_content_list: string[]; +} & StrapiSharedMetaData; + +export type StrapiSkill = { + title: string; + subtitle: string; + anchor_id: string; + entries: StrapiSkillEntry[]; +} & StrapiSharedMetaData; + +export type StrapiSkillEntry = { + title: string; + icon: string; + content: string[]; +} & StrapiSharedMetaData; + +export type StrapiContact = { + title: string; + subtitle: string; + form_title: string; + subject_placeholder: string; + message_placeholder: string; + cta_text: string; + anchor_id: string; +} & StrapiSharedMetaData; + +export type StrapiFooterResponse = { + data: StrapiFooter; + meta: object; +}; + +export type StrapiFooter = { + copyright: string; + linkedin: string; + git: string; +} & StrapiSharedMetaData; \ No newline at end of file diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..203bd9b --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,55 @@ +import { STRAPI_API_KEY } from '$env/static/private'; +import { PUBLIC_STRAPI_URL } from '$env/static/public'; +import type { LayoutServerLoad } from './$types'; +import { error } from '@sveltejs/kit'; +import { mapFooterData, mapHeaderData } from '$lib/mapping/strapiMapping.svelte.js'; +import type { StrapiComponentUnion } from '$lib/types/strapi'; + +export const load: LayoutServerLoad = async () => { + const query = new URLSearchParams({ + // Profile: category 'shared', component 'profile', relation 'profile', sub-field 'image' + 'populate[components][on][shared.profile][populate][profile][populate]': 'image', + + // Project: category 'shared', component 'project', relation 'project', sub-relation 'entries' + 'populate[components][on][shared.project][populate][project][populate]': 'entries', + + // Skill: category 'shared', component 'skill', relation 'skill', sub-relation 'entries' + 'populate[components][on][shared.skill][populate][skill][populate]': 'entries', + + // Contact: category 'shared', component 'contact', relation 'contact' + 'populate[components][on][shared.contact][populate]': 'contact' + }).toString(); + + const requestInit: RequestInit = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${STRAPI_API_KEY}` + } + }; + + const [homepageResponse, footerResponse] = await Promise.allSettled([ + fetch(`${PUBLIC_STRAPI_URL}/api/homepage?${query}`, requestInit), + fetch(`${PUBLIC_STRAPI_URL}/api/footer`, requestInit) + ]); + + if ( + homepageResponse.status === 'rejected' || + footerResponse.status === 'rejected' || + !homepageResponse.value.ok || + !footerResponse.value.ok + ) { + throw error(500, 'Failed to connect to the CMS'); + } + + const [homepageData, footerData] = await Promise.all([ + homepageResponse.value.json(), + footerResponse.value.json() + ]); + + return { + header: mapHeaderData(homepageData), + components: homepageData.data.components as StrapiComponentUnion[], + footer: mapFooterData(footerData) + }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index c4baccd..0161d55 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,12 +3,10 @@ import '$lib/styles/font.css'; import favicon from '$lib/assets/favicon.svg'; import Header from '$lib/components/Header.svelte'; - import content from '$lib/data/data.json'; - import type { Data } from '$lib/types/data'; import Footer from '$lib/components/Footer.svelte'; + import type { LayoutProps } from '../../.svelte-kit/types/src/routes/$types'; - let { children } = $props(); - let data: Data = content; + let { data, children }: LayoutProps = $props(); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7bffda2..c5ef0c0 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,15 +1,11 @@ - - - - \ No newline at end of file +{#each data.components as component, index (index)} + +{/each} \ No newline at end of file