ssr improve faq and marquee
This commit is contained in:
parent
fb73b7d2b5
commit
07851846ce
@ -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>
|
</template>
|
||||||
</div>
|
|
||||||
</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 toggle = (i: number) =>
|
||||||
|
(openIndex.value = openIndex.value === i ? null : i)
|
||||||
|
</script>
|
||||||
|
|
||||||
const { convertToHTML } = useHtmlConverter();
|
<style lang="sass">
|
||||||
const htmlContent = (data) => {
|
.myAccordion
|
||||||
return convertToHTML(data); // Nutze die convertToHTML Funktion der Composable
|
.accordion-item
|
||||||
};
|
border-bottom: 1px solid $lightgrey
|
||||||
|
width: 100%
|
||||||
|
|
||||||
// Props für die Items
|
.accordion-header
|
||||||
const props = defineProps({
|
all: unset
|
||||||
items: {
|
width: 100%
|
||||||
type: Array,
|
display: flex
|
||||||
required: true,
|
justify-content: space-between
|
||||||
default: () => [],
|
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'
|
||||||
|
|
||||||
// Zustand für das geöffnete Element
|
&:hover
|
||||||
const openIndex = ref(null);
|
background: linear-gradient(to left, white 1%, lighten($lightgrey, 2%) 80%, white 100%)
|
||||||
// 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
|
// Icon
|
||||||
const toggleSection = async (index) => {
|
.accordion-toggle
|
||||||
// Umschalten des offenen Indexes
|
position: relative
|
||||||
openIndex.value = openIndex.value === index ? null : index;
|
width: 20px
|
||||||
|
height: 20px
|
||||||
|
flex-shrink: 0
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
|
||||||
// Wenn ein neuer Abschnitt geöffnet wurde
|
span
|
||||||
if (openIndex.value !== null) {
|
position: absolute
|
||||||
await nextTick(); // Warten, bis das DOM aktualisiert wurde
|
width: 16px
|
||||||
|
height: 2px
|
||||||
|
background: #333
|
||||||
|
transition: transform .25s
|
||||||
|
|
||||||
setTimeout(() => {
|
&:first-child
|
||||||
const header = headerRefs.value[openIndex.value]; // Header des offenen Elements
|
transform: rotate(90deg) // horizontale Linie wird senkrecht
|
||||||
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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
&.open
|
||||||
|
span:first-child
|
||||||
|
transform: rotate(45deg)
|
||||||
|
span:last-child
|
||||||
|
transform: rotate(-45deg)
|
||||||
|
|
||||||
|
// Content
|
||||||
|
.accordion-content
|
||||||
|
padding: 0 1rem 1rem
|
||||||
|
|
||||||
// Setze die Referenzen dynamisch
|
// simple height-fade transition
|
||||||
const setContentRef = (el, index) => {
|
.accordion-enter-from,
|
||||||
if (el) {
|
.accordion-leave-to
|
||||||
contentRefs.value[index] = el;
|
max-height: 0
|
||||||
}
|
opacity: 0
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.myAccordion {
|
|
||||||
|
|
||||||
.accordion-item {
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accordion-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 1rem;
|
|
||||||
font-family: 'Mainfont-Bold';
|
|
||||||
background-color: white;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten(#EEEBE5, 6%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.accordion-title {
|
|
||||||
flex: 1; // Nimmt den freien Platz ein
|
|
||||||
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 {
|
|
||||||
position: absolute;
|
|
||||||
width: 15px; // Kürzere Linien für saubere Spitzen
|
|
||||||
height: 2px;
|
|
||||||
background-color: #333;
|
|
||||||
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease;
|
|
||||||
top: 8px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
transform: rotate(135deg); // Linie 1: Oben links zur Mitte
|
|
||||||
left: 5px; // Leicht nach links verschoben
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
transform: rotate(-135deg); // Linie 2: Oben rechts zur Mitte
|
|
||||||
left: -5px; // Leicht nach rechts verschoben
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
span:first-child {
|
|
||||||
transform: rotate(45deg); // Linie 1: Teil des "X"
|
|
||||||
left: 0; // Zentriert
|
|
||||||
}
|
|
||||||
|
|
||||||
span:last-child {
|
|
||||||
transform: rotate(-45deg); // Linie 2: Teil des "X"
|
|
||||||
left: 0; // Zentriert
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.accordion-enter-active,
|
||||||
|
.accordion-leave-active
|
||||||
|
transition: all .25s ease
|
||||||
|
|
||||||
|
.accordion-enter-to,
|
||||||
|
.accordion-leave-from
|
||||||
|
max-height: 500px // ausreichend groß
|
||||||
|
opacity: 1
|
||||||
|
</style>
|
||||||
|
|||||||
@ -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') }}
|
||||||
|
|||||||
@ -15,36 +15,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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 => ({
|
||||||
|
title: faq.question,
|
||||||
|
html: convertToHTML(faq.answer) // <- hier passiert die Umwandlung
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
// 🔹 **FAQs für die aktuelle Seite aus `pages` filtern**
|
const toggleContactBubble = () => mainStore.toggleContactBubble()
|
||||||
const accordionItems = computed(() => {
|
</script>
|
||||||
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,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
.faq
|
.faq
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
226
stores/main.ts
226
stores/main.ts
@ -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
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user