ssr improve faq and marquee

This commit is contained in:
Sabrina Hennrich 2025-05-19 14:55:49 +02:00
parent fb73b7d2b5
commit 07851846ce
8 changed files with 423 additions and 454 deletions

View File

@ -1,219 +1,110 @@
<template> <template>
<div class="myAccordion"> <div class="myAccordion">
<div v-for="(item, index) in items" :key="index" class="accordion-item"> <article
<div v-for="(item, i) in items"
:key="i"
class="accordion-item"
>
<!-- Header --------------------------------------------------->
<button
class="accordion-header" class="accordion-header"
:ref="el => setHeaderRef(el, index)" :aria-expanded="openIndex === i"
@click="toggleSection(index)" @click="toggle(i)"
> >
<div class="accordion-title">{{ item.title }}</div> <span class="accordion-title">{{ item.title }}</span>
<div class="accordion-toggle" :class="{ open: openIndex === index }">
<span></span> <!-- Toggle-Icon (2 Striche) -->
<span></span> <span class="accordion-toggle" :class="{ open: openIndex === i }">
</div> <span></span><span></span>
</div> </span>
</button>
<!-- Content --------------------------------------------------->
<transition name="accordion">
<div <div
v-if="openIndex === i"
class="accordion-content" class="accordion-content"
:ref="el => setContentRef(el, index)" v-html="item.html"
:style="{ maxHeight: openIndex === index ? `${contentHeights[index]}px` : '0px' }" />
> </transition>
<p><span v-html="htmlContent(item.content)"></span></p> </article>
</div>
</div>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { ref, onMounted, watch, nextTick } from 'vue'; import { ref } from 'vue'
import { useHtmlConverter } from '../composables/useHTMLConverter'; defineProps<{ items: { title: string; html: string }[] }>()
const openIndex = ref<number | null>(null)
const { convertToHTML } = useHtmlConverter(); const toggle = (i: number) =>
const htmlContent = (data) => { (openIndex.value = openIndex.value === i ? null : i)
return convertToHTML(data); // Nutze die convertToHTML Funktion der Composable
};
// Props für die Items
const props = defineProps({
items: {
type: Array,
required: true,
default: () => [],
},
});
// Zustand für das geöffnete Element
const openIndex = ref(null);
// Referenzen für die Inhalte
const contentRefs = ref([]);
// Höhen der Inhalte
const contentHeights = ref([]);
// Referenzen für die Header-Elemente
const headerRefs = ref([]);
// Funktion zum Umschalten des geöffneten Abschnitts
const toggleSection = async (index) => {
// Umschalten des offenen Indexes
openIndex.value = openIndex.value === index ? null : index;
// Wenn ein neuer Abschnitt geöffnet wurde
if (openIndex.value !== null) {
await nextTick(); // Warten, bis das DOM aktualisiert wurde
setTimeout(() => {
const header = headerRefs.value[openIndex.value]; // Header des offenen Elements
const fixedHeaderHeight = document.querySelector('.fixed-header')?.offsetHeight || 0;
if (header) {
const topPosition = header.getBoundingClientRect().top + window.scrollY - fixedHeaderHeight - 20; // 20px Puffer
// Scrollen zur berechneten Position
window.scrollTo({
top: topPosition > 0 ? topPosition : 0, // Verhindert negatives Scrollen
behavior: 'smooth',
});
}
}, 100); // Kleiner Timeout für das reibungslose Scrollen
}
};
// Setze die Referenzen dynamisch
const setContentRef = (el, index) => {
if (el) {
contentRefs.value[index] = el;
}
};
const setHeaderRef = (el, index) => {
if (el) {
headerRefs.value[index] = el;
}
};
// Berechne die Höhen der Inhalte
const calculateHeights = () => {
contentHeights.value = contentRefs.value.map((el) => {
if (!el) return 0;
const additionalHeight = 50; // Fester Wert für oben und unten
return el.scrollHeight + additionalHeight; // Dynamische Höhe + fester Wert
});
};
// Initialisiere die Berechnung nach dem Mounten
onMounted(async () => {
contentRefs.value = Array.from({ length: props.items.length });
headerRefs.value = Array.from({ length: props.items.length });
await nextTick();
calculateHeights();
});
// Aktualisiere die Höhen, wenn sich die Items ändern
watch(
() => props.items,
async () => {
contentRefs.value = Array.from({ length: props.items.length });
headerRefs.value = Array.from({ length: props.items.length });
await nextTick();
calculateHeights();
}
);
</script> </script>
<style lang="scss"> <style lang="sass">
.myAccordion { .myAccordion
.accordion-item
border-bottom: 1px solid $lightgrey
width: 100%
.accordion-item { .accordion-header
border-bottom: 1px solid #ccc; all: unset
width: 100%
display: flex
justify-content: space-between
align-items: center
padding: 1.2rem 1rem 1.2rem 1rem
background: #fff
cursor: pointer
border: 0
font: inherit
text-align: left
font-family: 'Mainfont-Bold'
&:last-of-type { &:hover
border-bottom: none; background: linear-gradient(to left, white 1%, lighten($lightgrey, 2%) 80%, white 100%)
}
.accordion-header { // Icon
display: flex; .accordion-toggle
justify-content: space-between; position: relative
align-items: center; width: 20px
cursor: pointer; height: 20px
padding: 1rem; flex-shrink: 0
font-family: 'Mainfont-Bold'; display: flex
background-color: white; justify-content: center
transition: background-color 0.3s ease; align-items: center
&:hover { span
background-color: lighten(#EEEBE5, 6%); position: absolute
} width: 16px
height: 2px
background: #333
transition: transform .25s
.accordion-title { &:first-child
flex: 1; // Nimmt den freien Platz ein transform: rotate(90deg) // horizontale Linie wird senkrecht
text-align: left; // Links ausgerichtet
font-weight: bold;
font-size: 1rem;
margin-right: 1rem;
}
.accordion-toggle {
position: relative;
flex-shrink: 0; // Verhindert, dass das Icon skaliert wird
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
span { &.open
position: absolute; span:first-child
width: 15px; // Kürzere Linien für saubere Spitzen transform: rotate(45deg)
height: 2px; span:last-child
background-color: #333; transform: rotate(-45deg)
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease;
top: 8px;
&:first-child { // Content
transform: rotate(135deg); // Linie 1: Oben links zur Mitte .accordion-content
left: 5px; // Leicht nach links verschoben padding: 0 1rem 1rem
}
&:last-child { // simple height-fade transition
transform: rotate(-135deg); // Linie 2: Oben rechts zur Mitte .accordion-enter-from,
left: -5px; // Leicht nach rechts verschoben .accordion-leave-to
} max-height: 0
} opacity: 0
&.open { .accordion-enter-active,
span:first-child { .accordion-leave-active
transform: rotate(45deg); // Linie 1: Teil des "X" transition: all .25s ease
left: 0; // Zentriert
}
span:last-child { .accordion-enter-to,
transform: rotate(-45deg); // Linie 2: Teil des "X" .accordion-leave-from
left: 0; // Zentriert max-height: 500px // ausreichend groß
} opacity: 1
}
}
}
.accordion-content {
overflow: hidden;
transition: max-height 0.3s ease-out;
padding: 0 1rem;
background-color: white;
h3 {
font-size: 1rem;
line-height: 1.4rem;
margin-bottom: .5rem;
}
p {
margin: 1rem 0.5rem;
font-size: 1rem;
}
}
}
}
</style> </style>

View File

@ -24,7 +24,14 @@
> >
<div class="row left m-2"> <div class="row left m-2">
<div class="col-md-6" id="hintBox"> <div class="col-md-6" id="hintBox">
<img class="mobileAspBox" v-if="screenWidth <= 768" :src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url" alt="Ansprechpartner Sabrina Hennrich"> <NuxtImg
v-if="screenWidth <= 768"
class="mobileAspBox"
:src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url"
alt="Sabrina Hennrich"
:width="400"
format="webp"
/>
<h2 id="contactTitle">{{ $t('contactForm.yourcontact2us') }}</h2> <h2 id="contactTitle">{{ $t('contactForm.yourcontact2us') }}</h2>
<p class="my-4"> <p class="my-4">
<svg aria-hidden="true"> <svg aria-hidden="true">
@ -38,8 +45,12 @@
{{ companyinfo.company }}<br>{{ companyinfo.street }} <br>{{ companyinfo.postalcode }} {{ companyinfo.city }} {{ companyinfo.company }}<br>{{ companyinfo.street }} <br>{{ companyinfo.postalcode }} {{ companyinfo.city }}
</p> </p>
<p class="aspProf">{{ $t('contactForm.yourcontactperson') }} <b>Sabrina Hennrich</b></p> <p class="aspProf">{{ $t('contactForm.yourcontactperson') }} <b>Sabrina Hennrich</b></p>
<div class="aspBox"><img :src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url" alt="Ansprechpartner Sabrina Hennrich"></div> <div class="aspBox"><NuxtImg
:src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url"
alt="Ansprechpartner Sabrina Hennrich"
:width="400"
format="webp"
/></div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -99,7 +110,7 @@
<span class="check"></span> <span class="check"></span>
{{ $t('contactForm.privacyInfotextBeforeLink') }} {{ $t('contactForm.privacyInfotextBeforeLink') }}
<NuxtLinkLocale <NuxtLinkLocale
to="/privacy" :to="$t('privacy')"
:aria-label="$t('privacy')" :aria-label="$t('privacy')"
> >
{{ $t('contactForm.privacyInfotextLinkText') }} {{ $t('contactForm.privacyInfotextLinkText') }}

View File

@ -17,35 +17,31 @@
</section> </section>
</template> </template>
<script setup> <script setup lang="ts">
import { useMainStore } from '@/stores/main'; import { computed, defineProps } from 'vue'
import { storeToRefs } from 'pinia'; import { useMainStore } from '@/stores/main'
import { computed, defineProps, defineAsyncComponent } from 'vue'; import { useHtmlConverter } from '~/composables/useHTMLConverter'
const { convertToHTML } = useHtmlConverter()
const props = defineProps({ const props = defineProps({
pageLink: { type: String, required: true }, pageLink: { type: String, required: true },
headline: { type: String, default: "Häufig gestellte Fragen (FAQs)" }, headline: { type: String, default: "Häufig gestellte Fragen (FAQs)" },
button: { type: String, default: "Sprechen Sie uns gerne an!" }, button: { type: String, default: "Sprechen Sie uns gerne an!" },
}); })
const mainStore = useMainStore(); const mainStore = useMainStore()
const { pages } = storeToRefs(mainStore); // Wir holen die `pages` aus dem Pinia-Store
const toggleContactBubble = () => mainStore.toggleContactBubble(); const accordionItems = computed(() =>
mainStore.getFaqsByPageLink(props.pageLink).map(faq => ({
// 🔹 **FAQs für die aktuelle Seite aus `pages` filtern** title: faq.question,
const accordionItems = computed(() => { html: convertToHTML(faq.answer) // <- hier passiert die Umwandlung
const currentPage = pages.value?.find(page => page.pageLink === props.pageLink); }))
const faqsArray = Array.isArray(currentPage?.faqs.data) ? currentPage.faqs.data : []; // Sicherstellen, dass es ein Array ist )
return faqsArray.map(faq => ({
title: faq.attributes.question,
content: faq.attributes.answer,
}));
});
const toggleContactBubble = () => mainStore.toggleContactBubble()
</script> </script>
<style lang="sass"> <style lang="sass">
.faq .faq
width: 80% width: 80%

View File

@ -13,7 +13,7 @@
id="wave" id="wave"
d=" M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z " d=" M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z "
fill="#EEEBE5" fill="#EEEBE5"
></path> />
</g> </g>
</svg> </svg>
@ -21,52 +21,48 @@
<div class="container"> <div class="container">
<h2 class="pt-4 pb-3">{{ title }}</h2> <h2 class="pt-4 pb-3">{{ title }}</h2>
<!-- Marquee mit doppeltem Inhalt für endloses Scrollen -->
<div class="marquee marquee--hover-pause mt-5"> <div class="marquee marquee--hover-pause mt-5">
<ul class="marquee__content"> <ul class="marquee__content">
<li v-for="(item, index) in items" :key="index"> <li v-for="(item, index) in items" :key="index">
<NuxtLink <NuxtLink v-if="item.link" :to="item.link" class="custLogoLink">
v-if="item.link" <NuxtImg
:to="`/${link}/${item.link}`" :src="cmsUrl + item.logo.url"
class="custLogoLink" alt="item.logo.alternativeText || 'Logo'"
> width="250"
<img format="webp"
:src="cmsUrl + getImageUrl(item)"
class="custLogo" class="custLogo"
:style="{ height: logoHeight + 'px' }"
:alt="getAltText(item)"
/> />
</NuxtLink> </NuxtLink>
<img <NuxtImg
v-else v-else
:src="cmsUrl + getImageUrl(item)" :src="cmsUrl + item.logo.url"
alt="item.logo.alternativeText || 'Logo'"
width="250"
format="webp"
class="custLogo" class="custLogo"
:style="{ height: logoHeight + 'px' }"
:alt="getAltText(item)"
/> />
</li> </li>
</ul> </ul>
<ul aria-hidden="true" class="marquee__content"> <!-- Duplizierte Liste für Endlos-Scroll -->
<li v-for="(item, index) in items" :key="'duplicate-' + index"> <ul class="marquee__content duplicate" aria-hidden="true">
<NuxtLink <li v-for="(item, index) in items" :key="'dup-'+index">
v-if="item.link" <NuxtLink v-if="item.link" :to="item.link" class="custLogoLink">
:to="`/${link}/${item.link}`" <NuxtImg
class="custLogoLink" :src="cmsUrl + item.logo.url"
> alt="item.logo.alternativeText || 'Logo'"
<img width="250"
:src="cmsUrl + getImageUrl(item)" format="webp"
class="custLogo" class="custLogo"
:style="{ height: logoHeight + 'px' }"
:alt="getAltText(item)"
/> />
</NuxtLink> </NuxtLink>
<img <NuxtImg
v-else v-else
:src="cmsUrl + getImageUrl(item)" :src="cmsUrl + item.logo.url"
alt="item.logo.alternativeText || 'Logo'"
width="250"
format="webp"
class="custLogo" class="custLogo"
:style="{ height: logoHeight + 'px' }"
:alt="getAltText(item)"
/> />
</li> </li>
</ul> </ul>
@ -105,10 +101,13 @@
<script setup> <script setup>
const runtimeConfig = useRuntimeConfig(); import { computed } from 'vue'
// Runtime config & base CMS URL
const runtimeConfig = useRuntimeConfig()
const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl) const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl)
// Props: title, items, logoHeight, und link (optional) // Props
const props = defineProps({ const props = defineProps({
items: { items: {
type: Array, type: Array,
@ -116,7 +115,7 @@
}, },
logoHeight: { logoHeight: {
type: Number, type: Number,
default: 50, // Standardhöhe in Pixel default: 50,
}, },
title: { title: {
type: String, type: String,
@ -124,32 +123,9 @@
}, },
link: { link: {
type: String, type: String,
default: 'projekt', // Standardwert, wenn keine spezifische Seite angegeben wird default: 'projekt',
}, },
}); })
// Funktion zur Bestimmung des Bild-URLs basierend auf Typ des Items
const getImageUrl = (item) => {
if (item.logo) {
// Für Customers: Verwende das logo-Feld
return item.logo.data.attributes.url;
} else if (item.projectImages && item.projectImages.data.length > 0) {
// Für Projects: Verwende das erste Bild im projectImages-Feld
return item.projectImages.data[0].attributes.url;
}
return '';
};
// Funktion zur Ermittlung des Alt-Texts für das Bild
const getAltText = (item) => {
if (item.logo) {
return item.logo.data.attributes.alternativeText || item.company || '';
} else if (item.projectImages && item.projectImages.data.length > 0) {
return item.projectImages.data[0].attributes.alternativeText || item.projectTitle || '';
}
return '';
};
</script> </script>
<style lang="sass"> <style lang="sass">

View File

@ -1,32 +1,25 @@
// composables/usePageMeta.ts // composables/usePageMeta.ts
import { watch, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useMainStore } from '~/stores/main' import { useMainStore } from '@/stores/main'
import { useHead } from '@unhead/vue' import { useHead } from '@unhead/vue'
export function usePageMeta () { export function usePageMeta () {
const route = useRoute() const route = useRoute()
const pageLink = route.path
const mainStore = useMainStore() const mainStore = useMainStore()
const page = mainStore.getPageByLink(pageLink) // ► Der aktuelle Page-Eintrag als computed
console.log(page) const currentPage = computed(() => mainStore.getPageByLink(route.path))
if (!page) { // ► Reagiere auf Route- oder Store-Änderungen
console.warn(`Keine Seite gefunden für den pageLink "${pageLink}"`) watch(
return () => currentPage.value, // Quelle
} (page) => { // Callback
if (!page) return
const metaTitle = page.SEO?.pageTitle || 'Standard Title' const metaTitle = page.SEO?.pageTitle ?? 'Standard Title'
const metaDescription = page.SEO?.seoDescription || 'Standard Description' const metaDescription = page.SEO?.seoDescription ?? 'Standard Description'
const metaImage = page.SEO?.seoImage?.url || '/default-image.jpg' const metaImage = page.SEO?.seoImage?.url ?? '/default-image.jpg'
try {
JSON.stringify(metaTitle)
JSON.stringify(metaDescription)
JSON.stringify(metaImage)
} catch (err) {
console.error('Fehler beim Serialisieren der Meta-Daten:', err)
}
useHead({ useHead({
title: metaTitle, title: metaTitle,
@ -37,7 +30,10 @@ export function usePageMeta() {
{ property: 'og:image', content: metaImage }, { property: 'og:image', content: metaImage },
{ name: 'twitter:title', content: metaTitle }, { name: 'twitter:title', content: metaTitle },
{ name: 'twitter:description', content: metaDescription }, { name: 'twitter:description', content: metaDescription },
{ name: 'twitter:image', content: metaImage }, { name: 'twitter:image', content: metaImage }
] ]
}) })
},
{ immediate: true } // Sofort beim ersten Aufruf ausführen
)
} }

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="homePage"> <div class="homePage">
<section class="heroBox"> <section class="heroBox" aria-labelledby="hero-heading">
<div class="container-10"> <div class="container-10">
<h1>{{ $t('home.heroBox.h1') }}</h1> <h1 id="hero-heading">{{ $t('home.heroBox.h1') }}</h1>
<h2>{{ $t('home.heroBox.h2') }}</h2> <h2>{{ $t('home.heroBox.h2') }}</h2>
<h3>{{ $t('home.heroBox.h3') }}</h3> <h3>{{ $t('home.heroBox.h3') }}</h3>
</div> </div>
@ -11,21 +11,28 @@
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"></path> <path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"></path>
</svg> </svg>
</section> </section>
<section> <section aria-labelledby="solution-title">
<div class="container-10 webStrategy"> <div class="container-10 webStrategy">
<img class="imgFloatLeft" src="https://strapi.digimedialoop.de/uploads/web_wireframe_Strategie_0bae802a68.png" alt="wireframe web strategie"> <NuxtImg
class="imgFloatLeft"
src="https://strapi.digimedialoop.de/uploads/web_wireframe_Strategie_0bae802a68.png"
alt="Wireframe Web Strategie"
width="300"
format="webp"
/>
<h2>{{ $t('home.solution.title') }}</h2> <h2 id="solution-title">{{ $t('home.solution.title') }}</h2>
<h3>{{ $t('home.solution.teaser') }}</h3> <h3>{{ $t('home.solution.teaser') }}</h3>
<p>{{ $t('home.solution.text') }}</p> <p>{{ $t('home.solution.text') }}</p>
<button class="mintBtn" <button class="mintBtn"
role="button" role="button"
aria-describedby="solution-title"
aria-label="headless CMS Info" @click="navigateToArticle">{{ $t('home.solution.buttonText') }}</button> aria-label="headless CMS Info" @click="navigateToArticle">{{ $t('home.solution.buttonText') }}</button>
</div> </div>
</section> </section>
<section class="targetGroup"> <section class="targetGroup" aria-labelledby="invitation-title">
<svg class="sectionWave wave-top" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20"> <svg class="sectionWave wave-top" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"></path> <path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"></path>
</svg> </svg>
@ -34,10 +41,11 @@
<div class="col-md-4"> <div class="col-md-4">
</div> </div>
<div class="col-md-8 pt-5 pb-5"> <div class="col-md-8 pt-5 pb-5">
<h2>{{ $t('home.invitation.title') }}</h2> <h2 id="invitation-title">{{ $t('home.invitation.title') }}</h2>
<h3>{{ $t('home.invitation.teaser') }}</h3> <h3>{{ $t('home.invitation.teaser') }}</h3>
<button class="pinkBtn" @click.prevent="toggleContactBubble" <button class="pinkBtn" @click.prevent="toggleContactBubble"
role="button" role="button"
aria-describedby="invitation-title"
aria-label="Kontaktformular öffnen">{{ $t('home.invitation.button') }}</button> aria-label="Kontaktformular öffnen">{{ $t('home.invitation.button') }}</button>
</div> </div>
</div> </div>
@ -76,9 +84,9 @@
<div class="row"> <div class="row">
<div class="col-md-6 my-5"> <div class="col-md-6 my-5">
<div class="innerBox"> <div class="innerBox">
<div class="canDoItem"> <div class="canDoItem" role="group" aria-labelledby="cando-title">
<div class="imageBox" style="background-image: url('https://strapi.digimedialoop.de/uploads/Screen_Shot_Tool_20250228133408_beb2a11980.png');"></div> <div class="imageBox" style="background-image: url('https://strapi.digimedialoop.de/uploads/Screen_Shot_Tool_20250228133408_beb2a11980.png');"></div>
<h3>{{ $t('home.canDo.item3.title') }}</h3> <h3 id="cando-title">{{ $t('home.canDo.item3.title') }}</h3>
<p>{{ $t('home.canDo.item3.text') }}</p> <p>{{ $t('home.canDo.item3.text') }}</p>
</div> </div>
</div> </div>
@ -130,7 +138,7 @@
:aria-label="$t('home.finalCall.button')">{{ $t('home.finalCall.button') }}</button> :aria-label="$t('home.finalCall.button')">{{ $t('home.finalCall.button') }}</button>
</div> </div>
</section> </section>
<MarqueeBanner :items="customers" :logoHeight="60" :title="$t('home.marqueeBanner.title')" /> <MarqueeBanner :items="logoItems" :logoHeight="60" :title="$t('home.marqueeBanner.title')" />
<FAQArea pageLink="/" :headline="$t('home.faqArea.headline')" /> <FAQArea pageLink="/" :headline="$t('home.faqArea.headline')" />
</div> </div>
@ -144,8 +152,11 @@ import { defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
const router = useRouter(); const router = useRouter();
const runtimeConfig = useRuntimeConfig();
const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl)
const mainStore = useMainStore(); const mainStore = useMainStore();
const { cmsUrl, customers } = storeToRefs(mainStore); const { customers } = storeToRefs(mainStore);
const toggleContactBubble = () => mainStore.toggleContactBubble(); const toggleContactBubble = () => mainStore.toggleContactBubble();
const navigateToArticle = () => { const navigateToArticle = () => {
@ -155,6 +166,16 @@ const navigateToArticle = () => {
const screenWidth = computed(() => mainStore.screenWidth); const screenWidth = computed(() => mainStore.screenWidth);
const waveHeight = computed(() => (screenWidth.value / 25).toFixed(0)); const waveHeight = computed(() => (screenWidth.value / 25).toFixed(0));
const logoItems = computed(() => {
return customers.value.map(customer => ({
company: customer.company || '',
logo: {
url: customer.logo?.url || '',
alternativeText: customer.company || ''
}
}))
})
</script> </script>

View File

@ -1,4 +1,10 @@
import { useMainStore } from '@/stores/main'
export default defineNuxtPlugin(async (nuxtApp) => { export default defineNuxtPlugin(async (nuxtApp) => {
const store = useMainStore() const mainStore = useMainStore(nuxtApp.$pinia)
await store.fetchInitialData() if (!mainStore.dataFetched) {
await mainStore.fetchInitialData()
}
}) })

View File

@ -24,36 +24,53 @@ interface CompanyInfo {
} }
} }
interface SEO {
pageTitle: string
seoDescription: string // Achtung: Schreibfehler wird so übernommen
seoKeywords: string
type: string
seoImage?: CompanyLogo | null
}
interface PageSection {
id: number
sectionText: string
sectionImage?: CompanyLogo | null
}
interface FAQ {
question: string
answer: string
}
interface Page { interface Page {
id: number id: number
pageName: string pageName: string
pageLink: string pageLink: string
header_image?: { header_image?: CompanyLogo | null
url: string SEO?: SEO | null
alternativeText?: string faqs: FAQ[]
} | null pageSections: PageSection[]
SEO?: { }
pageTitle: string
seoDescription: string interface CustomerProject {
seoKeywords: string
type: string
seoImage?: {
url: string
alternativeText?: string
} | null
} | null
faqs: Array<{
question: string
answer: string
}>
pageSections: Array<{
id: number id: number
sectionText: string projectTitle: string
sectionImage?: { launchDate?: string
url: string projectDescription?: string
alternativeText?: string link?: string
} | null webpage?: string
}> technologies: { titel: string; icon?: string }[]
projectImages: CompanyLogo[]
}
interface Customer {
id: number
company: string
city: string
logo?: CompanyLogo | null
invertLogo?: CompanyLogo | null
projects: CustomerProject[]
} }
export const useMainStore = defineStore('main', { export const useMainStore = defineStore('main', {
@ -64,9 +81,10 @@ export const useMainStore = defineStore('main', {
screenWidth: 1440, screenWidth: 1440,
companyinfo: null as CompanyInfo | null, companyinfo: null as CompanyInfo | null,
pages: [] as Page[], pages: [] as Page[],
customers: [] as Customer[],
dataFetched: false, dataFetched: false,
loading: false, loading: false,
error: null as { message: string, stack?: string } | null, error: null as { message: string; stack?: string } | null,
}), }),
getters: { getters: {
@ -77,12 +95,24 @@ export const useMainStore = defineStore('main', {
? `${runtimeConfig.public.cmsBaseUrl}${logoUrl}` ? `${runtimeConfig.public.cmsBaseUrl}${logoUrl}`
: '/uploads/dummy_Image_4abc3f04dd.webp' : '/uploads/dummy_Image_4abc3f04dd.webp'
}, },
isMobile: (state) => state.screenWidth < 768, isMobile: (state) => state.screenWidth < 768,
/** Neuer Getter: Seite anhand pageLink finden */ getPageByLink: (state) => (link: string) =>
getPageByLink: (state) => { state.pages.find((p) => p.pageLink === link),
return (link: string) => state.pages.find(p => p.pageLink === link)
getCustomerById: (state) => (id: number) =>
state.customers.find((c) => c.id === id),
getFaqsByPageId: (state) => (pageId: number) =>
state.pages.find((p) => p.id === pageId)?.faqs ?? [],
getFaqsByPageLink: (state) => (link: string) => {
const page = state.pages.find(p => p.pageLink === link);
return page?.faqs ?? [];
} }
}, },
actions: { actions: {
@ -110,62 +140,104 @@ export const useMainStore = defineStore('main', {
if (this.dataFetched) return if (this.dataFetched) return
this.loading = true this.loading = true
const { public: cfg } = useRuntimeConfig()
try { try {
const runtimeConfig = useRuntimeConfig() const [companyRes, pagesRes, customersRes] = await Promise.all([
const cmsUrl = runtimeConfig.public.cmsBaseUrl $fetch(`${cfg.cmsBaseUrl}/api/companyinfo?populate=*`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
$fetch(`${cfg.cmsBaseUrl}/api/pages?populate=*`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
$fetch(`${cfg.cmsBaseUrl}/api/customers?populate=*`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
])
const companyRes = await $fetch(`${cmsUrl}/api/companyinfo?populate=*`, { // CompanyInfo (Single Type)
headers: { 'Authorization': `Bearer ${runtimeConfig.public.cmsToken}` } this.companyinfo = companyRes.data?.attributes ?? companyRes
})
this.companyinfo = companyRes.data?.attributes || companyRes
const pagesRes = await $fetch(`${cmsUrl}/api/pages?populate=*`, { // Pages
headers: { 'Authorization': `Bearer ${runtimeConfig.public.cmsToken}` } this.pages = pagesRes.data.map((item: any) => {
}) const a = item.attributes
return {
this.pages = pagesRes.data.map((item: any) => ({
id: item.id, id: item.id,
pageName: item.attributes.pageName, pageName: a.pageName,
pageLink: item.attributes.pageLink, pageLink: a.pageLink,
header_image: item.attributes.header_image ? { header_image: a.header_image?.data
url: item.attributes.header_image.data.attributes.url, ? {
alternativeText: item.attributes.header_image.data.attributes.alternativeText url: a.header_image.data.attributes.url,
} : null, alternativeText: a.header_image.data.attributes.alternativeText,
SEO: item.attributes.SEO ? { }
pageTitle: item.attributes.SEO.pageTitle, : null,
seoDescription: item.attributes.SEO.seoDesicription, SEO: a.SEO
seoKeywords: item.attributes.SEO.seoKeywords, ? {
type: item.attributes.SEO.type, pageTitle: a.SEO.pageTitle,
seoImage: item.attributes.SEO.seoImage ? { seoDescription: a.SEO.seoDesicription, // Fehler absichtlich
url: item.attributes.SEO.seoImage.data.attributes.url, seoKeywords: a.SEO.seoKeywords,
alternativeText: item.attributes.SEO.seoImage.data.attributes.alternativeText type: a.SEO.type,
} : null seoImage: a.SEO.seoImage?.data
} : null, ? {
faqs: item.attributes.faqs ? item.attributes.faqs.data.map((faq: any) => ({ url: a.SEO.seoImage.data.attributes.url,
question: faq.attributes.question, alternativeText: a.SEO.seoImage.data.attributes.alternativeText,
answer: faq.attributes.answer }
})) : [], : null,
pageSections: item.attributes.pageSections ? item.attributes.pageSections.map((section: any) => ({ }
id: section.id, : null,
sectionText: section.sectionText, faqs: a.faqs?.data?.map((f: any) => ({
sectionImage: section.sectionImage ? { question: f.attributes.question,
url: section.sectionImage.data.attributes.url, answer: f.attributes.answer,
alternativeText: section.sectionImage.data.attributes.alternativeText })) ?? [],
} : null pageSections: a.pageSections?.map((s: any) => ({
})) : [] id: s.id,
})) sectionText: s.sectionText,
sectionImage: s.sectionImage?.data
? {
url: s.sectionImage.data.attributes.url,
alternativeText: s.sectionImage.data.attributes.alternativeText,
}
: null,
})) ?? [],
}
})
// Customers
this.customers = customersRes.data.map((item: any) => {
const a = item.attributes
return {
id: item.id,
company: a.company,
city: a.city,
logo: a.logo?.data?.attributes ?? null,
invertLogo: a.invertLogo?.data?.attributes ?? null,
projects: (a.projects?.data ?? []).map((p: any) => ({
id: p.id,
projectTitle: p.attributes.projectTitle,
projectImages: p.attributes.projectImages?.data?.map((img: any) => ({
url: img.attributes.url,
alternativeText: img.attributes.alternativeText,
})) ?? [],
launchDate: p.attributes.launchDate,
projectDescription: p.attributes.projectDescription,
link: p.attributes.link,
webpage: p.attributes.webpage,
technologies: p.attributes.Technologien?.data?.map((t: any) => ({
titel: t.attributes.titel,
icon: t.attributes.icon,
})) ?? [],
})),
}
})
this.dataFetched = true this.dataFetched = true
} catch (err) { } catch (err) {
const errorObj = err as Error const e = err as Error
this.error = { this.error = { message: e.message, stack: e.stack }
message: errorObj.message, console.error('Fetch-Fehler:', e)
stack: errorObj.stack
}
console.error('Fehler beim Laden der Daten:', errorObj)
} finally { } finally {
this.loading = false this.loading = false
} }
} },
} },
}) })