dev test
This commit is contained in:
parent
3e302c5449
commit
f929763892
@ -110,6 +110,14 @@ body
|
||||
width: 80%
|
||||
margin: auto 10%
|
||||
|
||||
.container-15
|
||||
width: 70%
|
||||
margin: auto 15%
|
||||
|
||||
.container-20
|
||||
width: 60%
|
||||
margin: auto 20%
|
||||
|
||||
.fade-enter-active, .fade-leave-active
|
||||
transition: opacity 1.2s ease
|
||||
|
||||
@ -155,7 +163,22 @@ body
|
||||
transform: translateY(200px)
|
||||
opacity: 0
|
||||
|
||||
|
||||
/* Welle oben */
|
||||
.sectionWave.wave-top
|
||||
position: absolute
|
||||
left: 0
|
||||
width: 100%
|
||||
z-index: 1
|
||||
transform: scaleY(1) scaleX(-1)
|
||||
top: -2px
|
||||
/* Welle unten */
|
||||
.sectionWave.wave-bottom
|
||||
position: absolute
|
||||
left: 0
|
||||
width: 100%
|
||||
z-index: 1
|
||||
transform: scaleY(-1)
|
||||
bottom: -2px
|
||||
|
||||
|
||||
// +++++++++++++++++
|
||||
|
||||
@ -1,118 +1,157 @@
|
||||
<template>
|
||||
<nav v-if="breadcrumbs && breadcrumbs.length" class="breadcrumbs" aria-label="Breadcrumb">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link to="/" aria-label="Startseite">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" aria-hidden="true" focusable="false">
|
||||
<title>Startseite</title>
|
||||
<path d="M575.8 255.5c0 18-15 32.1-32 32.1l-32 0 .7 160.2c0 2.7-.2 5.4-.5 8.1l0 16.2c0 22.1-17.9 40-40 40l-16 0c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1L416 512l-24 0c-22.1 0-40-17.9-40-40l0-24 0-64c0-17.7-14.3-32-32-32l-64 0c-17.7 0-32 14.3-32 32l0 64 0 24c0 22.1-17.9 40-40 40l-24 0-31.9 0c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2l-16 0c-22.1 0-40-17.9-40-40l0-112c0-.9 0-1.9 .1-2.8l0-69.7-32 0c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"/>
|
||||
</svg>
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-for="(crumb, index) in breadcrumbs" :key="index">
|
||||
<router-link v-if="index < breadcrumbs.length - 1" :to="crumb.to">
|
||||
{{ crumb.label }}
|
||||
</router-link>
|
||||
<span v-else>{{ crumb.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
<nav v-if="breadcrumbs.length" class="breadcrumbs" aria-label="Breadcrumb">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link to="/" aria-label="Startseite">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" aria-hidden="true" focusable="false">
|
||||
<title>Startseite</title>
|
||||
<path d="M575.8 255.5c0 18-15 32.1-32 32.1l-32 0 .7 160.2c0 2.7-.2 5.4-.5 8.1l0 16.2c0 22.1-17.9 40-40 40l-16 0c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1L416 512l-24 0c-22.1 0-40-17.9-40-40l0-24 0-64c0-17.7-14.3-32-32-32l-64 0c-17.7 0-32 14.3-32 32l0 64 0 24c0 22.1-17.9 40-40 40l-24 0-31.9 0c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2l-16 0c-22.1 0-40-17.9-40-40l0-112c0-.9 0-1.9 .1-2.8l0-69.7-32 0c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"/>
|
||||
</svg>
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-for="(crumb, index) in breadcrumbs" :key="index">
|
||||
<router-link v-if="index < breadcrumbs.length - 1" :to="crumb.to" :title="crumb.labelFull">
|
||||
{{ crumb.label }}
|
||||
</router-link>
|
||||
<span v-else :title="crumb.labelFull">{{ crumb.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { i18nPages } from '~/i18n/i18n-pages'
|
||||
|
||||
export default {
|
||||
name: 'Breadcrumb',
|
||||
computed: {
|
||||
breadcrumbs() {
|
||||
const locale = this.$i18n.locale // aktives Sprachpräfix (z. B. "en", "de", etc.)
|
||||
const pathWithoutLang = this.$route.path.replace(`/${locale}`, '') // Sprachprefix entfernen
|
||||
interface Breadcrumb {
|
||||
label: string
|
||||
labelFull: string
|
||||
to: string
|
||||
}
|
||||
|
||||
const pathArray = pathWithoutLang.split('/').filter(p => p)
|
||||
let path = ''
|
||||
return pathArray.map(segment => {
|
||||
path += '/' + segment
|
||||
return {
|
||||
label: this.formatLabel(segment),
|
||||
to: `/${locale}${path}` // Sprachprefix im Link wieder einfügen
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatLabel(segment) {
|
||||
return segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ')
|
||||
}
|
||||
}
|
||||
function formatLabel(segment: string): { label: string; labelFull: string } {
|
||||
const labelFull = segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ')
|
||||
let label = labelFull
|
||||
if (label.length > 25) {
|
||||
label = label.slice(0, 22) + '...'
|
||||
}
|
||||
return { label, labelFull }
|
||||
}
|
||||
|
||||
function buildUrl(loc: string, path: string): string {
|
||||
if (loc === 'de') {
|
||||
return path.startsWith('/') ? path : '/' + path
|
||||
}
|
||||
return `/${loc}${path.startsWith('/') ? path : '/' + path}`
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const { locale, t } = useI18n()
|
||||
|
||||
const breadcrumbs = computed<Breadcrumb[]>(() => {
|
||||
const loc = locale.value
|
||||
const pathWithoutLang = route.path.replace(`/${loc}`, '')
|
||||
const segments = pathWithoutLang.split('/').filter(Boolean)
|
||||
|
||||
if (segments.length === 2 && segments[0] === 'projekt') {
|
||||
const referencesPath = i18nPages.references?.[loc] || '/references'
|
||||
|
||||
// Übersetzung für "references" holen, Fallback zu englisch falls nicht vorhanden
|
||||
const referencesLabel = t('references') || 'References'
|
||||
|
||||
const first = {
|
||||
label: referencesLabel,
|
||||
labelFull: referencesLabel,
|
||||
to: buildUrl(loc, referencesPath)
|
||||
}
|
||||
|
||||
const { label, labelFull } = formatLabel(segments[1])
|
||||
const second = {
|
||||
label,
|
||||
labelFull,
|
||||
to: route.path
|
||||
}
|
||||
|
||||
return [first, second]
|
||||
}
|
||||
|
||||
let path = ''
|
||||
return segments.map(segment => {
|
||||
path += '/' + segment
|
||||
const { label, labelFull } = formatLabel(segment)
|
||||
return {
|
||||
label,
|
||||
labelFull,
|
||||
to: buildUrl(loc, path)
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="sass">
|
||||
.breadcrumbs
|
||||
position: fixed
|
||||
top: 22vh
|
||||
left: 0
|
||||
text-align: left
|
||||
padding: 1rem .25rem 1rem .5rem
|
||||
//min-width: 200px
|
||||
background-color: rgba(white, .98)
|
||||
border: 1px solid darken($lightgrey, 5%)
|
||||
writing-mode: vertical-rl
|
||||
transform: rotate(180deg)
|
||||
//border-bottom-left-radius: .65rem
|
||||
border-top-left-radius: .8rem
|
||||
border-bottom-left-radius: .8rem
|
||||
text-transform: uppercase
|
||||
letter-spacing: .05rem
|
||||
z-index: 22
|
||||
ul
|
||||
<style lang="sass" scoped>
|
||||
.breadcrumbs
|
||||
position: fixed
|
||||
top: 22vh
|
||||
left: 0
|
||||
text-align: left
|
||||
padding: 1rem .25rem 1rem .5rem
|
||||
background-color: rgba(white, .98)
|
||||
border: 1px solid darken($lightgrey, 5%)
|
||||
writing-mode: vertical-rl
|
||||
transform: rotate(180deg)
|
||||
border-top-left-radius: .8rem
|
||||
border-bottom-left-radius: .8rem
|
||||
text-transform: uppercase
|
||||
letter-spacing: .05rem
|
||||
z-index: 22
|
||||
ul
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
list-style: none
|
||||
padding: 0
|
||||
margin: 0
|
||||
gap: 0.5rem
|
||||
cursor: pointer
|
||||
|
||||
li
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
list-style: none
|
||||
padding: 0
|
||||
margin: 0
|
||||
gap: 0.5rem
|
||||
cursor: pointer
|
||||
align-items: center
|
||||
color: darken($primaryColor, 10%)
|
||||
font-size: .7rem
|
||||
|
||||
li
|
||||
display: flex
|
||||
align-items: center
|
||||
color: darken($primaryColor, 10%)
|
||||
font-size: .7rem
|
||||
svg
|
||||
width: .8rem
|
||||
height: .8rem
|
||||
transform: rotate(90deg)
|
||||
margin-bottom: .35rem
|
||||
transition: .3s
|
||||
|
||||
svg
|
||||
width: .8rem
|
||||
height: .8rem
|
||||
transform: rotate(90deg)
|
||||
margin-bottom: .35rem
|
||||
transition: .3s
|
||||
path
|
||||
fill: darken($lightgrey, 20%)
|
||||
|
||||
path
|
||||
fill: darken($lightgrey, 20%)
|
||||
&:hover
|
||||
transform: scale(1.3) rotate(90deg)
|
||||
path
|
||||
fill: darken($lightgrey, 30%)
|
||||
|
||||
&::after
|
||||
content: '>'
|
||||
margin: 0 0.5rem
|
||||
color: $pink
|
||||
|
||||
&:last-child::after
|
||||
content: ''
|
||||
|
||||
a
|
||||
color: #007BFF
|
||||
text-decoration: none
|
||||
|
||||
&:hover
|
||||
transform: scale(1.3) rotate(90deg)
|
||||
path
|
||||
fill: darken($lightgrey, 30%)
|
||||
|
||||
&::after
|
||||
content: '>'
|
||||
margin: 0 0.5rem
|
||||
color: $pink
|
||||
|
||||
&:last-child::after
|
||||
content: ''
|
||||
|
||||
a
|
||||
color: #007BFF
|
||||
text-decoration: none
|
||||
|
||||
&:hover
|
||||
transform: scale(1.2)
|
||||
|
||||
span
|
||||
font-weight: bold
|
||||
</style>
|
||||
transform: scale(1.2)
|
||||
|
||||
span
|
||||
font-weight: bold
|
||||
</style>
|
||||
|
||||
33
components/CallToActionBox.vue
Normal file
33
components/CallToActionBox.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="ctaBox">
|
||||
<h3>{{ headline }}</h3>
|
||||
<p>{{ text }}</p>
|
||||
<button class="pinkBtn mt-1" @click.prevent="toggleContactBubble" role="button">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useMainStore } from '@/stores/main'
|
||||
|
||||
const mainStore = useMainStore()
|
||||
|
||||
const toggleContactBubble = () => {
|
||||
mainStore.toggleContactBubble()
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
headline: { type: String, required: true },
|
||||
text: { type: String, required: true },
|
||||
buttonText: { type: String, required: true }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
.ctaBox
|
||||
margin: 5vh 10%
|
||||
h3
|
||||
font-size: 1.6rem
|
||||
</style>
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
afterImage: { type: String, required: true }
|
||||
})
|
||||
|
||||
const sliderValue = ref(60) // Startposition in der Mitte
|
||||
const sliderValue = ref(80) // Startposition in der Mitte
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@ -92,7 +92,7 @@
|
||||
width: 20px // Breite des Schiebereglers
|
||||
height: 20px // Höhe des Schiebereglers
|
||||
border-radius: 50%
|
||||
background: $pink
|
||||
background: $primaryColor
|
||||
border: 1px solid #fff
|
||||
cursor: pointer
|
||||
z-index: 15
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
<h2 class="pt-4 pb-3">{{ title }}</h2>
|
||||
|
||||
<div class="marquee">
|
||||
<div class="marquee-track">
|
||||
<div class="marquee-track" :style="`animation-duration: ${speed}s`">
|
||||
<ul class="marquee-list">
|
||||
<li
|
||||
v-for="(item, index) in items"
|
||||
@ -31,10 +31,11 @@
|
||||
<NuxtLink v-if="item.link" :to="item.link" class="custLogoLink">
|
||||
<NuxtImg
|
||||
provider="strapi"
|
||||
:src="item.logo.url"
|
||||
:alt="item.logo.alternativeText || 'Logo'"
|
||||
:src="item.image.url"
|
||||
:alt="item.image.alternativeText || item.text || 'Image'"
|
||||
:title="item.text"
|
||||
width="250"
|
||||
height="50"
|
||||
:height="logoHeight"
|
||||
format="webp"
|
||||
loading="lazy"
|
||||
class="custLogo"
|
||||
@ -43,13 +44,15 @@
|
||||
<NuxtImg
|
||||
v-else
|
||||
provider="strapi"
|
||||
:src="item.logo.url"
|
||||
:alt="item.logo.alternativeText || 'Logo'"
|
||||
:src="item.image.url"
|
||||
:alt="item.image.alternativeText || item.text || 'Image'"
|
||||
:title="item.text"
|
||||
width="250"
|
||||
height="50"
|
||||
format="webp"
|
||||
loading="lazy"
|
||||
class="custLogo"
|
||||
:class="['custLogo', { greyscale: greyscale }]"
|
||||
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
@ -86,6 +89,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl)
|
||||
@ -94,12 +98,26 @@ const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
logoHeight: {
|
||||
type: [String, Number],
|
||||
default: 50 // bisheriger Wert in Pixel
|
||||
},
|
||||
speed: {
|
||||
type: [String, Number],
|
||||
default: 20 // bisheriger Wert in Sekunden
|
||||
},
|
||||
greyscale: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@ -144,9 +162,12 @@ const props = defineProps({
|
||||
.custLogo
|
||||
width: auto
|
||||
max-width: 250px
|
||||
height: 50px
|
||||
transition: filter 0.3s ease, transform .5s ease
|
||||
&:hover
|
||||
transform: scale(1.1)
|
||||
|
||||
.greyscale
|
||||
filter: grayscale(100%)
|
||||
transition: filter 0.3s ease
|
||||
&:hover
|
||||
filter: grayscale(0)
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ const screenWidth = computed(() => mainStore.screenWidth)
|
||||
height: auto
|
||||
z-index: 12
|
||||
&.mobile
|
||||
top: 45vh
|
||||
top: 20vh
|
||||
|
||||
|
||||
</style>
|
||||
67
components/SideBarNaviSlider.vue
Normal file
67
components/SideBarNaviSlider.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="sideBarNaviSlider"
|
||||
:class="{ 'slide-in': showSideBar }"
|
||||
@click="navigate">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { navigateTo } from '#app'
|
||||
|
||||
// Props für dynamischen Inhalt und Link
|
||||
const props = defineProps({
|
||||
link: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// Animation für das Einfahren
|
||||
const showSideBar = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
showSideBar.value = true
|
||||
}, 100) // Verzögerung für weiche Animation
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
showSideBar.value = false
|
||||
})
|
||||
|
||||
// Funktion zur Navigation in Nuxt 3
|
||||
const navigate = () => {
|
||||
if (props.link) {
|
||||
navigateTo(props.link)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.sideBarNaviSlider
|
||||
position: fixed
|
||||
background-color: rgba($yellow, .8)
|
||||
color: white
|
||||
text-transform: uppercase
|
||||
font-size: 1rem !important
|
||||
letter-spacing: .05rem
|
||||
top: 40vh
|
||||
right: -80px // Startposition außerhalb des Bildschirms
|
||||
padding: 1.2rem .8rem 1.6rem .8rem
|
||||
writing-mode: vertical-lr
|
||||
transform: rotate(180deg)
|
||||
border-bottom-right-radius: 1rem
|
||||
border-top-right-radius: 1rem
|
||||
cursor: pointer
|
||||
transition: right 0.6s ease-out, transform .8s
|
||||
z-index: 10
|
||||
|
||||
&.slide-in
|
||||
right: 0 // Fährt an die finale Position
|
||||
|
||||
&:hover
|
||||
transform: rotate(180deg) scale(1.1)
|
||||
</style>
|
||||
|
||||
@ -158,7 +158,7 @@ footer
|
||||
position: relative
|
||||
width: 100vw
|
||||
color: white
|
||||
z-index: 10
|
||||
z-index: 9
|
||||
height: auto
|
||||
min-height: 120px
|
||||
margin-top: 100px
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
// composables/useI18nPages.ts
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { i18nPages } from '@/i18n/i18n-pages'
|
||||
|
||||
@ -6,10 +5,23 @@ export function useI18nPages () {
|
||||
const { locale } = useI18n()
|
||||
|
||||
const getRoute = (key: keyof typeof i18nPages) => {
|
||||
return i18nPages[key][locale.value]
|
||||
const entry = i18nPages[key]
|
||||
if (!entry) return ''
|
||||
|
||||
if ('paths' in entry) {
|
||||
return entry.paths[locale.value]
|
||||
}
|
||||
|
||||
return entry[locale.value]
|
||||
}
|
||||
|
||||
const getProjectLink = (slug: string) => {
|
||||
const path = getRoute('projekt___link')
|
||||
return path.replace(':link', slug)
|
||||
}
|
||||
|
||||
return {
|
||||
getRoute
|
||||
getRoute,
|
||||
getProjectLink
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,6 +39,16 @@ export const i18nPages = {
|
||||
es: '/aviso-legal',
|
||||
tr: '/künye'
|
||||
},
|
||||
'projekt___link': {
|
||||
paths: {
|
||||
de: '/projekt/:link',
|
||||
en: '/projekt/:link',
|
||||
fr: '/projekt/:link',
|
||||
it: '/projekt/:link',
|
||||
es: '/projekt/:link',
|
||||
tr: '/projekt/:link'
|
||||
}
|
||||
},
|
||||
privacy: {
|
||||
de: '/datenschutz',
|
||||
en: '/privacy',
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"services": "Leistungen",
|
||||
"contact": "Kontakt",
|
||||
"references": "Referenzen",
|
||||
"referenceoverview": "Referenzübersicht",
|
||||
"imprint": "Impressum",
|
||||
"privacy": "Datenschutz",
|
||||
"privacyPolicy": "Datenschutzerklärung",
|
||||
@ -130,8 +131,48 @@
|
||||
"title": "Du bist Grafikdesigner oder Mediengestalter?",
|
||||
"button": "Hier unser Angebot für Dich"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"hero": {
|
||||
"ariaLabel": "Einleitungsbereich über digitale Lösungen",
|
||||
"imageAlt": "Illustration von Programmierung am Ammersee",
|
||||
"headline1": "High-Performance-Webseiten",
|
||||
"headline2": "mit moderner Headless-Architektur",
|
||||
"headline3": "Schnell, effizient und leistungsstark!"
|
||||
},
|
||||
"explain": {
|
||||
"ariaLabel": "Warum unsere Webseiten mehr können als nur gut aussehen",
|
||||
"headline1": "Wir entwickeln Webseiten für Mensch und Maschine",
|
||||
"headline2": "Design das Menschen begeistert mit Struktur die Maschinen verstehen",
|
||||
"paragraph": "Damit eine Webseite heute erfolgreich ist, muss sie beides leisten: Emotionen wecken und von Algorithmen verstanden werden. Mit klarer Struktur, sauberer Semantik und starker Performance schaffen wir die Basis für Sichtbarkeit und nachhaltiges Wachstum.",
|
||||
"bullet1": "Schnelle Ladezeiten durch moderne Headless-Technologien",
|
||||
"bullet2": "Optimiert für Google, Bing & KI durch saubere Semantik",
|
||||
"bullet3": "Barrierefrei entwickelt, damit zugänglich für alle Nutzer",
|
||||
"bullet4": "Einfach erweiterbar dank modularer Architektur",
|
||||
"bullet5": "SEO-freundlich mit klarer Struktur und strukturierten Daten"
|
||||
|
||||
},
|
||||
"marquee": {
|
||||
"title": "Erleben Sie unsere Projekte live!"
|
||||
},
|
||||
"ctaBox": {
|
||||
"headline": "Sie wollen mehr Speed und Leistung für Ihr Business?",
|
||||
"text": "Wir bringen Ihre Website auf das nächste Level! Schnell, zuverlässig und individuell.",
|
||||
"button": "Sprechen Sie mit uns!"
|
||||
}
|
||||
},
|
||||
"references": {
|
||||
"hero": {
|
||||
"h1": "Individuelle Webseiten, die überzeugen",
|
||||
"h2": "Lassen Sie sich von unseren erfolgreichen Webprojekten inspirieren!",
|
||||
"p": "Jede Website, die wir entwickeln, ist einzigartig – maßgeschneidert, funktional und wirkungsvoll. Unsere Referenzen zeigen, wie wir modernes Webdesign, durchdachte Entwicklung und gute Performance verbinden, um digitale Lösungen zu schaffen, die nicht nur gut aussehen, sondern auch Ergebnisse liefern."
|
||||
},
|
||||
"ctaBox": {
|
||||
"headline": "Bereit für Ihr eigenes Webprojekt?",
|
||||
"text": "Lassen Sie uns gemeinsam Ihre individuelle Website gestalten – perfekt abgestimmt auf Ihre Bedürfnisse und Ziele.",
|
||||
"button": "Jetzt unverbindliches Angebot anfordern!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -9,94 +9,127 @@
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"termsOfService": "Terms of Service",
|
||||
"terms": "Terms",
|
||||
"faq": "FAQ",
|
||||
"magazin": "Insights",
|
||||
"faq": "Frequently Asked Questions",
|
||||
"magazin": "Knowledge",
|
||||
"accessability": "Accessibility",
|
||||
"accessibilitySettings": "Accessibility Settings",
|
||||
"changeFontSize": "Increase text size",
|
||||
"greyscale": "Greyscale",
|
||||
"increaseContrast": "Increase contrast",
|
||||
"borderFocus": "Enable focus highlight",
|
||||
"hideImages": "Hide images",
|
||||
"showLinks": "Highlight links",
|
||||
"infoAccessibility": "Information about the accessibility of our site",
|
||||
"changeFontSize": "Increase Text Size",
|
||||
"greyscale": "Grayscale",
|
||||
"increaseContrast": "Increase Contrast",
|
||||
"borderFocus": "Enable Focus",
|
||||
"hideImages": "Hide Images",
|
||||
"showLinks": "Highlight Links",
|
||||
"infoAccessibility": "Information about accessibility on our site",
|
||||
"importantLinks": "Important Links",
|
||||
"contactForm": {
|
||||
"yourcontact2us": "Get in touch with us!",
|
||||
"ourOffice": "Our Office Address",
|
||||
"yourcontactperson": "Your Contact Person",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"message": "Message",
|
||||
"company": "Company",
|
||||
"sendMessage": "Send Message",
|
||||
"privacyInfotextBeforeLink": "By submitting the form, you agree to the storage of your data on our server for the purpose of contacting you.",
|
||||
"privacyInfotextLinkText": "Privacy Policy",
|
||||
"validation": {
|
||||
"nameRequired": "Name is a required field.",
|
||||
"emailOrPhoneRequired": "Please enter either an email address or a phone number.",
|
||||
"invalidEmail": "Please enter a valid email address.",
|
||||
"invalidPhone": "Please enter a valid phone number."
|
||||
},
|
||||
"successMessage": "Your message has been sent successfully.",
|
||||
"errorMessage": "There is currently a problem with the internet connection!",
|
||||
"confirmation": {
|
||||
"thx": "Thank you for your message!",
|
||||
"info": "We will get back to you shortly...",
|
||||
"salutation": "Your digimedialoop Team"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"heroBox": {
|
||||
"h1": "Your agency for custom web design and professional web development",
|
||||
"h2": "Modular websites using the latest technologies",
|
||||
"h3": "Making your website fast, efficient and future-proof!"
|
||||
},
|
||||
"solution": {
|
||||
"title": "How your website becomes a real business tool",
|
||||
"teaser": "We develop custom websites with JAMstack technology tailored to your business, serving as a powerful marketing and sales tool for your success.",
|
||||
"text": "By clearly separating content and technology and using a headless content management system, we create low-maintenance, SEO-optimized solutions that are not only scalable long-term but also make work easier for your marketing team: content can be managed without technical barriers, new features integrated flexibly – without plugin chaos or interfering with the live system.",
|
||||
"buttonText": "Learn more about Headless CMS"
|
||||
},
|
||||
"invitation": {
|
||||
"title": "Is your website ready for the future?",
|
||||
"teaser": "We'll show you how to optimize your digital presence, effectively reach your target audience, and benefit long-term from our scalable, low-maintenance solutions. In a free initial consultation, you’ll learn exactly what steps are needed to turn your website into a powerful marketing tool.",
|
||||
"button": "Request your free initial consultation!"
|
||||
},
|
||||
"canDo": {
|
||||
"title": "Start using your website’s full potential!",
|
||||
"item1": {
|
||||
"title": "Gain new customers and increase revenue",
|
||||
"text": "Turn visitors into paying customers! With a clear strategy, compelling design and optimized user experience, your website becomes a lead machine."
|
||||
"yourcontact2us": "Your contact to us!",
|
||||
"ourOffice": "Our office address",
|
||||
"yourcontactperson": "Your contact person",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"message": "Message",
|
||||
"company": "Company",
|
||||
"sendMessage": "Send message",
|
||||
"privacyInfotextBeforeLink": "By submitting the form, you agree to the storage of your data on our server for the purpose of contacting you.",
|
||||
"privacyInfotextLinkText": "Information about data protection",
|
||||
"validation": {
|
||||
"nameRequired": "Name is a required field.",
|
||||
"emailOrPhoneRequired": "Please enter either an email address or a phone number.",
|
||||
"invalidEmail": "Please enter a valid email address.",
|
||||
"invalidPhone": "Please enter a valid phone number."
|
||||
},
|
||||
"item2": {
|
||||
"title": "Retain customers and members",
|
||||
"text": "Strengthen your customer relationships! With valuable content, exclusive offers and interactive features, your target group remains active and engaged."
|
||||
},
|
||||
"item3": {
|
||||
"title": "Attract and inspire new employees",
|
||||
"text": "Find the right talent! An authentic career page with clear benefits makes your company irresistible to applicants."
|
||||
},
|
||||
"item4": {
|
||||
"title": "Reduce administrative effort",
|
||||
"text": "Fewer inquiries – more efficiency! With clear information and digital processes on your website, you save time and costs while easing the workload for your team."
|
||||
"successMessage": "Your message has been sent successfully.",
|
||||
"errorMessage": "Unfortunately, there is currently an error with the internet connection!",
|
||||
"confirmation": {
|
||||
"thx": "Thank you for your message!",
|
||||
"info": "We will get back to you promptly...",
|
||||
"salutation": "Your digimedialoop team"
|
||||
}
|
||||
},
|
||||
"faqBox": {
|
||||
"questions": "Questions?",
|
||||
"faqsDefault": "Frequently Asked Questions (FAQs)",
|
||||
"btnDefault": "Feel free to contact us!"
|
||||
},
|
||||
"pages": {
|
||||
"home": {
|
||||
"heroBox": {
|
||||
"h1": "Your agency for individual web design and professional web development",
|
||||
"h2": "Modular websites with the latest technologies",
|
||||
"h3": "Highest performance – fast, efficient, and future-proof!"
|
||||
},
|
||||
"solution": {
|
||||
"title": "Performance, AI Compatibility & Accessibility",
|
||||
"teaser": "We develop tailor-made websites based on modern JAMstack technology, perfectly tailored to your requirements.",
|
||||
"text": "By clearly separating content and technology, using a headless content management system like Strapi, maintenance-friendly, SEO-optimized solutions are created that are not only scalable in the long term but also make work easier for your team. Content can be maintained without technical hurdles, and new features integrated flexibly – all without plugin chaos or interfering with the live system. Thanks to clean semantic structure, our solutions are also optimally prepared for AI-supported search systems and allow easy integration into AI-powered operator workflows.",
|
||||
"buttonText": "Learn more about Headless CMS"
|
||||
},
|
||||
"invitation": {
|
||||
"title": "Is your website ready for the future?",
|
||||
"teaser": "We show you how to optimize your digital presence, effectively reach your target audience, and benefit long-term from our scalable, maintenance-friendly solutions. During a free initial consultation, you will learn exactly which steps are necessary to transform your website into a powerful marketing tool.",
|
||||
"button": "Request free initial consultation!"
|
||||
},
|
||||
"canDo": {
|
||||
"title": "You too can fully leverage your website’s potential in the future!",
|
||||
"item1": {
|
||||
"title": "Gain new customers and increase revenue",
|
||||
"text": "Turn visitors into paying customers! With a clear strategy, convincing design, and optimized user guidance, your website becomes a lead machine."
|
||||
},
|
||||
"item2": {
|
||||
"title": "Retain customers and members",
|
||||
"text": "Strengthen relationships with your customers! With valuable content, exclusive offers, and interactive features, your audience stays active and engaged."
|
||||
},
|
||||
"item3": {
|
||||
"title": "Find and inspire employees",
|
||||
"text": "Attract the right talents! An authentic career page with clear benefits makes your company irresistible to applicants."
|
||||
},
|
||||
"item4": {
|
||||
"title": "Reduce administrative effort",
|
||||
"text": "Fewer inquiries – more efficiency! Clear information and digital processes on your website save time, costs, and relieve your team."
|
||||
}
|
||||
},
|
||||
"compBox": {
|
||||
"title": "\"Design is the art of combining function and aesthetics\"",
|
||||
"subtitle": "With this claim, we start the relaunch process for our clients.",
|
||||
"text": "We place special value on a clean design that corresponds to users’ mental models – so visitors always find exactly what they are looking for, where they expect it."
|
||||
},
|
||||
"finalCall": {
|
||||
"title": "Together, we take your business to the next level!",
|
||||
"button": "Contact us!"
|
||||
},
|
||||
"marqueeBanner": {
|
||||
"title": "These companies trust us"
|
||||
},
|
||||
"faqArea": {
|
||||
"headline": "Here you will find answers to frequently asked questions (FAQs) about website creation with digimedialoop"
|
||||
}
|
||||
},
|
||||
"webagency": {
|
||||
"hero": {
|
||||
"title": "Your web agency for strategic web development and functional web design in Herrsching am Ammersee",
|
||||
"subtitle": "We develop websites that turn visitors into customers!",
|
||||
"text1": "We stand for professional, innovative, and strategic web solutions and combine technical know-how with a deep understanding of digital communication to successfully position companies online.",
|
||||
"text2": "Our approach is always individual: Every project is realized with care, foresight, and the latest technologies. We rely on close cooperation and tailor-made solutions that fit our clients. We accompany companies from various industries – from small businesses to larger firms – on their way to a successful online presence.",
|
||||
"text3": "Let us optimize your digital presence together!",
|
||||
"button": "Your contact to us!"
|
||||
},
|
||||
"team": {
|
||||
"title": "Your contact person at digimedialoop",
|
||||
"name": "Sabrina Hennrich",
|
||||
"position": "Consulting | Concept | Design | Development",
|
||||
"text1": "With over 20 years of experience in web design, she is still a web developer out of pure passion!",
|
||||
"text2": "After graduating as a business economist, she worked many years in marketing before deepening her knowledge with a psychology degree.",
|
||||
"text3": "This combination of business know-how, strategic marketing experience, and psychological understanding enables her to develop digital solutions that are not only aesthetically appealing but also target-effective and economically well thought-out.",
|
||||
"text4": "Additionally, since 2019 she is also a Certified Expert in User Experience & Usability, giving her profound knowledge in user-centered design and optimal usability.",
|
||||
"quote": "Openness, transparency, and fairness are extremely important to me when working with my clients and partners. I only recommend what makes sense to me and fits my clients. For this, I gladly take the time for a thorough analysis of my clients’ needs or those of their target group.",
|
||||
"button": "Feel free to contact me!"
|
||||
},
|
||||
"grafiker": {
|
||||
"supheadline": "digimedialoop for creatives",
|
||||
"title": "Are you a graphic designer or media designer?",
|
||||
"button": "Here is our offer for you"
|
||||
}
|
||||
}
|
||||
},
|
||||
"compBox": {
|
||||
"title": "\"Design is the art of uniting function and aesthetics\"",
|
||||
"subtitle": "This is our approach when starting a client’s relaunch process.",
|
||||
"text": "We place great emphasis on a clean design that aligns with users’ mental models – so visitors always find exactly what they’re looking for, right where they expect it."
|
||||
},
|
||||
"finalCall": {
|
||||
"title": "Together, we’ll take your business to the next level!",
|
||||
"button": "Contact us!"
|
||||
},
|
||||
"marqueeBanner": {
|
||||
"title": "These companies trust us"
|
||||
},
|
||||
"faqArea": {
|
||||
"headline": "Here you’ll find answers to frequently asked questions (FAQs) about website creation with digimedialoop"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,6 +158,23 @@ main
|
||||
border: none
|
||||
padding: .4rem .8rem
|
||||
margin: 0 0 1rem 0
|
||||
|
||||
.check
|
||||
list-style: none
|
||||
padding: .2rem 1rem
|
||||
margin: 0
|
||||
|
||||
li
|
||||
position: relative
|
||||
padding-left: 1.5em
|
||||
margin: .8em 0
|
||||
|
||||
&::before
|
||||
content: "\2713"
|
||||
position: absolute
|
||||
left: 0
|
||||
color: $primaryColor
|
||||
|
||||
section
|
||||
margin-bottom: 5vh
|
||||
position: relative
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="homePage">
|
||||
<section class="heroBox" aria-labelledby="hero-heading">
|
||||
<section class="heroBox_service" aria-labelledby="hero-heading">
|
||||
<NuxtImg
|
||||
provider="strapi"
|
||||
src="/uploads/large_DML_Home_Hero_4f27bc7f8e.webp"
|
||||
@ -138,7 +138,7 @@ class="pinkBtn mt-3"
|
||||
@click.prevent="toggleContactBubble">{{ $t('pages.home.finalCall.button') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
<MarqueeBanner :items="logoItems" :logo-height="60" :title="$t('pages.home.marqueeBanner.title')" />
|
||||
<MarqueeBanner :items="logoItems" :logo-height="60" :title="$t('pages.home.marqueeBanner.title')" :greyscale="true" />
|
||||
<FAQArea page-link="/" :headline="$t('pages.home.faqArea.headline')" />
|
||||
|
||||
</div>
|
||||
@ -168,13 +168,14 @@ const waveHeight = computed(() => (screenWidth.value / 25).toFixed(0));
|
||||
|
||||
const logoItems = computed(() => {
|
||||
return customers.value.map(customer => ({
|
||||
company: customer.company || '',
|
||||
logo: {
|
||||
text: customer.company || '',
|
||||
image: {
|
||||
url: customer.logo?.url || '',
|
||||
alternativeText: customer.company || ''
|
||||
alternativeText: customer.logo?.alternativeText || customer.company || ''
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
const canDoItems = [
|
||||
{
|
||||
img: '/uploads/website_Erfolg_Marketing_3c36a43ba5.png',
|
||||
@ -208,7 +209,7 @@ const canDoItems = [
|
||||
<style lang="sass">
|
||||
.homePage
|
||||
|
||||
.heroBox
|
||||
.heroBox_service
|
||||
position: relative
|
||||
min-height: 35rem
|
||||
height: 70vh
|
||||
@ -267,22 +268,7 @@ const canDoItems = [
|
||||
.container
|
||||
z-index: 2
|
||||
|
||||
/* Welle oben */
|
||||
.sectionWave.wave-top
|
||||
position: absolute
|
||||
left: 0
|
||||
width: 100%
|
||||
z-index: 1
|
||||
transform: scaleY(1) scaleX(-1)
|
||||
top: -2px
|
||||
/* Welle unten */
|
||||
.sectionWave.wave-bottom
|
||||
position: absolute
|
||||
left: 0
|
||||
width: 100%
|
||||
z-index: 1
|
||||
transform: scaleY(-1)
|
||||
bottom: -2px
|
||||
|
||||
|
||||
.webStrategy
|
||||
padding: 4rem 0 3.5rem 0
|
||||
@ -349,8 +335,11 @@ const canDoItems = [
|
||||
max-width: 50%
|
||||
.homeImageTop
|
||||
margin: 4.5rem 0 8vh 3rem !important
|
||||
|
||||
.compBox
|
||||
background-image: linear-gradient(to bottom left, white, #FEDEE8, white)
|
||||
background-image: url(https://strapi.digimedialoop.de/uploads/bubbles_DM_Lmint_trans_08ddb0a921.webp) //linear-gradient(to bottom left, white, #FEDEE8, white)
|
||||
background-size: cover
|
||||
background-repeat: no-repeat
|
||||
padding: 5rem 0 3rem 0
|
||||
h2
|
||||
font-family: 'Comfortaa'
|
||||
|
||||
251
pages/projekt/[link].vue
Normal file
251
pages/projekt/[link].vue
Normal file
@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<section class="project topSpace">
|
||||
<!-- SideBarNaviSlider mit dynamischem Link und i18n-Label -->
|
||||
<SideBarNaviSlider :link="localePath('references')">
|
||||
{{ t('referenceoverview') }}
|
||||
</SideBarNaviSlider>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<h1>{{ t('project.detail.title', 'Kundenprojektvorstellung') }}</h1>
|
||||
<h2>{{ project.projectTitle }}</h2>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="customerBox">
|
||||
<NuxtImg
|
||||
:src="customer.logo.url"
|
||||
:alt="customer.logo.alternativeText"
|
||||
provider="strapi"
|
||||
/>
|
||||
{{ }}
|
||||
<h4>
|
||||
{{ project.customer.company }} |
|
||||
{{ project.customer.city }}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row detailBox">
|
||||
<div class="col-lg-4">
|
||||
<transition name="fade" mode="out-in">
|
||||
<NuxtImg
|
||||
v-if="currentImage"
|
||||
id="currentImage"
|
||||
:src="currentImage.url"
|
||||
:alt="currentImage.alternativeText || project.projectTitle"
|
||||
provider="strapi"
|
||||
/>
|
||||
</transition>
|
||||
<div class="preview">
|
||||
<h3>{{ t('project.detail.moreViews', 'Weitere Ansichten') }}</h3>
|
||||
<div class="imageNavigation">
|
||||
<NuxtImg
|
||||
v-for="(img, index) in project.projectImages"
|
||||
:key="index"
|
||||
:src="img.url"
|
||||
:alt="img.alternativeText"
|
||||
provider="strapi"
|
||||
@click="setCurrentImage(img)"
|
||||
:class="{ active: currentImage?.url === img.url }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-lg-8 pt-4">
|
||||
<span v-html="htmlContent(project.projectDescription)"></span>
|
||||
|
||||
|
||||
<h4>{{ t('project.detail.technologies', 'Verwendete Technologien') }}</h4>
|
||||
<div class="techChipsBox">
|
||||
<span
|
||||
v-for="(tech, index) in project.technologies"
|
||||
:key="index"
|
||||
class="techChip"
|
||||
>
|
||||
{{ tech.titel }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row" v-if="project.webpage">
|
||||
<div class="col-12 text-end">
|
||||
<a
|
||||
class="webPageBtn"
|
||||
:href="project.webpage"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg>
|
||||
<use xlink:href="/assets/icons/collection.svg#desktop" />
|
||||
</svg>
|
||||
{{ t('project.detail.visitProject', 'Projekt live erleben') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue"
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
const localePath = useLocalePath()
|
||||
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useMainStore } from '@/stores/main'
|
||||
const mainStore = useMainStore()
|
||||
const { cmsUrl, projects, dataFetched } = storeToRefs(mainStore)
|
||||
const project = computed(() => mainStore.getProjectByLink(route.params.link))
|
||||
const customer = computed(() => {
|
||||
if (!project.value || !project.value.customer) return null
|
||||
return mainStore.getCustomerById(project.value.customer.id)
|
||||
})
|
||||
const currentImage = ref(null)
|
||||
|
||||
if (project.value && project.value.projectImages?.length > 0) {
|
||||
currentImage.value = project.value.projectImages[0]
|
||||
}
|
||||
|
||||
watch(project, (newProject) => {
|
||||
if (newProject && newProject.projectImages?.length > 0) {
|
||||
currentImage.value = newProject.projectImages[0]
|
||||
} else {
|
||||
currentImage.value = null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
import { useHtmlConverter } from '@/composables/useHTMLConverter'
|
||||
const { convertToHTML } = useHtmlConverter()
|
||||
const htmlContent = (data: string) => {
|
||||
return convertToHTML(data)
|
||||
}
|
||||
|
||||
// Setze das aktuelle Bild
|
||||
const setCurrentImage = (image: any) => {
|
||||
currentImage.value = image
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.project
|
||||
h1
|
||||
color: $pink //adjust-color($darkgrey, $lightness: 40%)
|
||||
font-size: 1.1rem
|
||||
text-transform: uppercase
|
||||
margin-bottom: 0
|
||||
letter-spacing: .08rem
|
||||
h2
|
||||
margin-top: 0
|
||||
img
|
||||
width: 100%
|
||||
.preview
|
||||
h3
|
||||
font-size: 1rem
|
||||
color: adjust-color($darkgrey, $lightness: 40%)
|
||||
img
|
||||
width: 100px
|
||||
margin: 0
|
||||
cursor: pointer
|
||||
transition: .6s
|
||||
padding: 1.2rem
|
||||
border: 2px solid transparent
|
||||
&.active
|
||||
border: 2px solid $lightgrey
|
||||
padding: .5rem
|
||||
border-radius: .5rem
|
||||
.customerBox
|
||||
width: 100%
|
||||
max-width: 50vw
|
||||
text-align: center
|
||||
background-image: linear-gradient(to left bottom, rgba($lightgrey, .6), transparent, transparent)
|
||||
border-top-right-radius: 20px
|
||||
padding: 1rem
|
||||
border-top: 1px solid rgba($lightgrey, .3)
|
||||
border-right: 1px solid rgba($lightgrey, .3)
|
||||
margin: 1rem 0
|
||||
img
|
||||
min-height: 2rem
|
||||
width: 50%
|
||||
max-width: 200px
|
||||
margin: 1rem
|
||||
h4
|
||||
font-size: .8rem
|
||||
@media(max-width: $breakPointLG)
|
||||
background-image: linear-gradient(to left, rgba($lightgrey, .6), transparent, transparent)
|
||||
margin-top: 0
|
||||
.detailBox
|
||||
h4
|
||||
font-size: 1rem
|
||||
margin-top: 2.5rem
|
||||
color: adjust-color($darkgrey, $lightness: 20%)
|
||||
font-family: 'Mainfont-Bold'
|
||||
.webPageBtn
|
||||
font-size: .8rem
|
||||
margin-top: 2rem
|
||||
margin-right: 6%
|
||||
text-decoration: none
|
||||
border: 1px solid adjust-color($darkgrey, $lightness: 20%)
|
||||
padding: .5rem 1rem
|
||||
border-radius: 5px
|
||||
display: inline-block
|
||||
color: adjust-color($darkgrey, $lightness: 30%)
|
||||
transition: .6s
|
||||
&:hover
|
||||
transform: scale(1.1)
|
||||
svg
|
||||
height: .8rem
|
||||
width: .9rem
|
||||
margin-right: .3rem
|
||||
fill: adjust-color($darkgrey, $lightness: 20%)
|
||||
.ctaBox
|
||||
padding: 3rem 0
|
||||
h2
|
||||
margin-bottom: .5rem
|
||||
h3
|
||||
margin-bottom: .5rem
|
||||
.navigationBox
|
||||
margin-top: 2rem
|
||||
width: 100%
|
||||
color: adjust-color($darkgrey, $lightness: 35%)
|
||||
font-size: .85rem
|
||||
|
||||
&:hover
|
||||
cursor: pointer
|
||||
.navBtn
|
||||
transition: .6s
|
||||
&:hover
|
||||
transform: scale(1.05)
|
||||
span
|
||||
display: inline-block
|
||||
svg
|
||||
fill: adjust-color($lightgrey, $lightness: -10%)
|
||||
width: 80%
|
||||
max-width: 50px
|
||||
.techChipsBox
|
||||
display: block
|
||||
width: 100%
|
||||
.techChip
|
||||
background-color: $lightgrey
|
||||
padding: .2rem 1rem
|
||||
margin: .3rem
|
||||
border-radius: .6rem
|
||||
font-size: .9rem
|
||||
display: inline-block
|
||||
color: adjust-color($darkgrey, $lightness: 25%)
|
||||
</style>
|
||||
|
||||
@ -1,15 +1,183 @@
|
||||
<template>
|
||||
<div class="container-10">
|
||||
<h1>{{ $t('references') }}</h1>
|
||||
</div>
|
||||
<div>
|
||||
<section class="topSpace">
|
||||
<div class="container">
|
||||
<h1>{{ $t('pages.references.hero.h1') }}</h1>
|
||||
<p v-html="$t('pages.references.hero.p')"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="referenceBox" v-if="projects && projects.length">
|
||||
<slot>
|
||||
<NuxtLink
|
||||
class="reference"
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
:to="getProjectLink(project.link)"
|
||||
>
|
||||
<NuxtImg
|
||||
provider="strapi"
|
||||
:src="project.projectImages?.[0]?.url"
|
||||
:alt="project.projectImages?.[0]?.alternativeText || project.projectTitle"
|
||||
width="200"
|
||||
class="project-image"
|
||||
priority
|
||||
/>
|
||||
<div class="infoBox">
|
||||
<div class="info-content">
|
||||
<h2>{{ project.projectTitle }}</h2>
|
||||
</div>
|
||||
<div class="logo-wrapper">
|
||||
<NuxtImg
|
||||
provider="strapi"
|
||||
:src="getCustomerLogo(project.customer?.id)"
|
||||
:alt="getCustomerAlt(project.customer?.id)"
|
||||
height="50"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</slot>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CallToActionBox
|
||||
:headline="$t('pages.references.ctaBox.headline')"
|
||||
:text="$t('pages.references.ctaBox.text')"
|
||||
:buttonText="$t('pages.references.ctaBox.button')"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: 'default'
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const cmsUrl = runtimeConfig.public.cmsBaseUrl
|
||||
|
||||
const mainStore = useMainStore()
|
||||
const { projects } = storeToRefs(mainStore)
|
||||
|
||||
import { useI18nPages } from '@/composables/useI18nPages'
|
||||
const { getProjectLink } = useI18nPages()
|
||||
|
||||
function getCustomerLogo(customerId) {
|
||||
if (!customerId) return ''
|
||||
const customer = mainStore.customers?.find(c => c.id === customerId)
|
||||
return customer?.logo?.url || ''
|
||||
}
|
||||
|
||||
function getCustomerAlt(customerId) {
|
||||
if (!customerId) return ''
|
||||
const customer = mainStore.customers?.find(c => c.id === customerId)
|
||||
return customer?.logo?.alternativeText || 'Kundenlogo'
|
||||
}
|
||||
|
||||
function toggleContactBubble() {
|
||||
console.log('Kontaktanfrage öffnen')
|
||||
}
|
||||
|
||||
// Erstelle das JSON-LD für alle Projekte
|
||||
const jsonLdProjects = computed(() => {
|
||||
if (!projects.value || !Array.isArray(projects.value) || projects.value.length === 0) return null;
|
||||
|
||||
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
||||
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ItemList",
|
||||
"itemListElement": projects.value.map((project, index) => ({
|
||||
"@type": "ListItem",
|
||||
"position": index + 1,
|
||||
"url": origin + (project.link ? getProjectLink(project.link) : ''),
|
||||
"name": project.projectTitle || 'Projekt',
|
||||
"image": cmsUrl + (project.projectImages?.[0]?.url) || '',
|
||||
"description": project.projectDescription?.[0]?.children?.[0]?.text || ''
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
// useHead einbinden, wenn Projekte da sind
|
||||
watchEffect(() => {
|
||||
if (jsonLdProjects.value) {
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
children: JSON.stringify(jsonLdProjects.value)
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
<style lang="sass" scoped>
|
||||
.referenceBox
|
||||
display: grid
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))
|
||||
gap: 4rem
|
||||
width: 80%
|
||||
margin: 0 10% 5rem auto
|
||||
|
||||
h2
|
||||
font-size: 1.4rem
|
||||
|
||||
.reference
|
||||
display: flex
|
||||
flex-direction: column
|
||||
position: relative
|
||||
text-decoration: none
|
||||
color: $darkgrey
|
||||
background: white
|
||||
border-radius: 10px
|
||||
overflow: hidden
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease
|
||||
&:hover
|
||||
transform: scale(1.02)
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1)
|
||||
|
||||
img
|
||||
width: 90%
|
||||
height: auto
|
||||
object-fit: cover
|
||||
margin: 1rem 5% 0 5%
|
||||
transition: transform 0.6s ease
|
||||
&:hover
|
||||
transform: scale(1.05)
|
||||
|
||||
.infoBox
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: space-between
|
||||
padding: 1rem 1.5rem
|
||||
border-bottom-left-radius: 10px
|
||||
border-bottom-right-radius: 10px
|
||||
background: linear-gradient(to bottom right, white 40%, $lightgrey)
|
||||
min-height: 8rem
|
||||
|
||||
.info-content
|
||||
flex: 2
|
||||
h2
|
||||
font-size: 1rem
|
||||
margin: 0.5rem 0
|
||||
line-height: 1.2
|
||||
hyphens: auto
|
||||
color: $darkgrey
|
||||
|
||||
.logo-wrapper
|
||||
flex: 1
|
||||
display: flex
|
||||
justify-content: flex-end
|
||||
align-items: center
|
||||
|
||||
img
|
||||
max-height: 50px
|
||||
max-width: 100%
|
||||
object-fit: contain
|
||||
|
||||
</style>
|
||||
@ -1,15 +1,243 @@
|
||||
<template>
|
||||
<div class="container-10">
|
||||
<h1>{{ $t('services') }}</h1>
|
||||
</div>
|
||||
<div>
|
||||
<section class="heroBox" :aria-label="$t('pages.services.hero.ariaLabel')">
|
||||
<NuxtImg
|
||||
provider="strapi"
|
||||
src="/uploads/BG_technology_b6b0083811.png"
|
||||
class="hero-bg"
|
||||
sizes="sm:100vw md:100vw lg:100vw"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
priority
|
||||
loading="eager"
|
||||
preload
|
||||
fetchpriority="high"
|
||||
/>
|
||||
<div class="container-10">
|
||||
<h1>{{ $t('pages.services.hero.headline1') }}</h1>
|
||||
<h2>{{ $t('pages.services.hero.headline2') }}</h2>
|
||||
<h3>{{ $t('pages.services.hero.headline3') }}</h3>
|
||||
</div>
|
||||
<!-- Nach dem Container: Spiegelwelle unten -->
|
||||
<svg class="sectionWave wave-bottom" style="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20" aria-hidden="true">
|
||||
<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"/>
|
||||
</svg>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="explainBox" :aria-label="$t('pages.services.explain.ariaLabel')">
|
||||
<NuxtImg
|
||||
src="/uploads/Human_Maschine_BG_5c91e9100f.webp"
|
||||
provider="strapi"
|
||||
format="webp"
|
||||
sizes="100vw"
|
||||
class="background-image"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="container-15 content">
|
||||
<h2>{{ $t('pages.services.explain.headline1') }}</h2>
|
||||
<h3>{{ $t('pages.services.explain.headline2') }}</h3>
|
||||
|
||||
<p>{{ $t('pages.services.explain.paragraph') }}</p>
|
||||
<ul class="check">
|
||||
<li>{{ t('pages.services.explain.bullet1') }}</li>
|
||||
<li>{{ t('pages.services.explain.bullet2') }}</li>
|
||||
<li>{{ t('pages.services.explain.bullet3') }}</li>
|
||||
<li>{{ t('pages.services.explain.bullet4') }}</li>
|
||||
<li>{{ t('pages.services.explain.bullet5') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<MarqueeBanner
|
||||
:items="projectItems"
|
||||
:logoHeight="200"
|
||||
:title="$t('pages.services.marquee.title')"
|
||||
link="projekt"
|
||||
:aria-label="$t('pages.services.marquee.title')"
|
||||
speed="15"
|
||||
/>
|
||||
|
||||
<CallToActionBox
|
||||
:headline="$t('pages.services.ctaBox.headline')"
|
||||
:text="$t('pages.services.ctaBox.text')"
|
||||
:buttonText="$t('pages.services.ctaBox.button')"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: 'default'
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useMainStore } from '@/stores/main';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const origin = runtimeConfig.public.appUrl
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const mainStore = useMainStore();
|
||||
const { projects, companyinfo } = storeToRefs(mainStore)
|
||||
|
||||
const projectItems = computed(() => {
|
||||
return projects.value
|
||||
.filter(project => project.customer && project.projectImages.length > 0)
|
||||
.map(project => ({
|
||||
text: project.customer?.company || '',
|
||||
image: {
|
||||
url: project.projectImages[0]?.url || '',
|
||||
alternativeText: project.projectImages[0]?.alternativeText || project.customer?.company || ''
|
||||
},
|
||||
link: project.link || ''
|
||||
}))
|
||||
})
|
||||
|
||||
const logoUrl = computed(() => {
|
||||
if (!companyinfo) return origin + '/logo.svg';
|
||||
return companyinfo.logo?.data?.attributes?.url
|
||||
? origin + companyinfo.logo.data.attributes.url
|
||||
: origin + '/logo.svg';
|
||||
})
|
||||
|
||||
// JSON_LD für Services
|
||||
const jsonLdServices = computed(() => {
|
||||
|
||||
if (!companyinfo?.value || !companyinfo.value.company) return null;
|
||||
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfessionalService",
|
||||
"name": companyinfo.value.company,
|
||||
"url": origin,
|
||||
"logo": logoUrl.value || (origin + '/logo.svg'),
|
||||
"description": "Spezialisiert auf JAMstack-Webentwicklung, Headless CMS-Integration und moderne Frontend-Lösungen.",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": companyinfo.value.street || '',
|
||||
"addressLocality": companyinfo.value.city || '',
|
||||
"addressRegion": "Bayern",
|
||||
"postalCode": companyinfo.value.postalcode || '',
|
||||
"addressCountry": "DE"
|
||||
},
|
||||
"hasOfferCatalog": {
|
||||
"@type": "OfferCatalog",
|
||||
"name": "Leistungen",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "Offer",
|
||||
"itemOffered": {
|
||||
"@type": "Service",
|
||||
"name": "JAMstack-Webentwicklung",
|
||||
"description": "Moderne Webentwicklung mit statischen Seiten und Headless CMS."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Offer",
|
||||
"itemOffered": {
|
||||
"@type": "Service",
|
||||
"name": "Headless CMS Integration",
|
||||
"description": "Flexible CMS-Lösungen für maximale Freiheit im Frontend."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Offer",
|
||||
"itemOffered": {
|
||||
"@type": "Service",
|
||||
"name": "Frontend-Entwicklung",
|
||||
"description": "Moderne Frontend-Frameworks und UI/UX Design."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (companyinfo.value && jsonLdServices.value) {
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
children: JSON.stringify(jsonLdServices.value)
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
.heroBox
|
||||
position: relative
|
||||
min-height: 35rem
|
||||
height: 70vh
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
overflow: hidden
|
||||
|
||||
.hero-bg
|
||||
position: absolute
|
||||
inset: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
object-fit: cover
|
||||
object-position: center center
|
||||
z-index: 0
|
||||
|
||||
.container-10, h1, h2, h3
|
||||
position: relative
|
||||
z-index: 1
|
||||
h1, h2, h3
|
||||
color: white
|
||||
z-index: 2
|
||||
line-height: 1.5
|
||||
max-width: 70%
|
||||
@media (max-width: $breakPointMD)
|
||||
max-width: 100%
|
||||
h1
|
||||
margin-top: 0
|
||||
font-size: clamp(2rem, 1.4rem + 3vw, 2.8rem)
|
||||
margin-bottom: 0
|
||||
font-family: 'Comfortaa'
|
||||
h2
|
||||
font-size: clamp(1.2rem, .7rem + 2vw, 2rem)
|
||||
margin: .8rem 0 .8rem 0
|
||||
font-family: 'Comfortaa'
|
||||
h3
|
||||
font-size: 1.2rem
|
||||
|
||||
.explainBox
|
||||
position: relative
|
||||
overflow: hidden
|
||||
min-height: 400px
|
||||
margin: 5vh 0
|
||||
padding: 8vh 0
|
||||
|
||||
.background-image
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: auto
|
||||
object-fit: contain
|
||||
object-position: center center
|
||||
z-index: 0
|
||||
pointer-events: none
|
||||
|
||||
.content
|
||||
position: relative
|
||||
z-index: 1
|
||||
padding-left: 10%
|
||||
|
||||
h3
|
||||
font-family: 'Mainfont-Bold'
|
||||
font-size: 1.2rem
|
||||
|
||||
</style>
|
||||
@ -214,7 +214,7 @@ const navigateTo = useRouter().push;
|
||||
@media(max-width: $breakPointSM)
|
||||
padding-left: 0
|
||||
h2
|
||||
color: darken($lightgrey, 50%)
|
||||
color: darken($lightgrey, 30%)
|
||||
font-size: .9rem
|
||||
margin-bottom: 1rem
|
||||
h3
|
||||
|
||||
@ -26,7 +26,7 @@ interface CompanyInfo {
|
||||
|
||||
interface SEO {
|
||||
pageTitle: string
|
||||
seoDescription: string // Achtung: Schreibfehler wird so übernommen
|
||||
seoDescription: string
|
||||
seoKeywords: string
|
||||
type: string
|
||||
seoImage?: CompanyLogo | null
|
||||
@ -62,6 +62,11 @@ interface CustomerProject {
|
||||
webpage?: string
|
||||
technologies: { titel: string; icon?: string }[]
|
||||
projectImages: CompanyLogo[]
|
||||
customer?: {
|
||||
id: number
|
||||
company: string
|
||||
city: string
|
||||
} | null
|
||||
}
|
||||
|
||||
interface Customer {
|
||||
@ -82,6 +87,7 @@ export const useMainStore = defineStore('main', {
|
||||
companyinfo: null as CompanyInfo | null,
|
||||
pages: [] as Page[],
|
||||
customers: [] as Customer[],
|
||||
projects: [] as CustomerProject[],
|
||||
dataFetched: false,
|
||||
loading: false,
|
||||
error: null as { message: string; stack?: string } | null,
|
||||
@ -95,43 +101,34 @@ export const useMainStore = defineStore('main', {
|
||||
? `${runtimeConfig.public.cmsBaseUrl}${logoUrl}`
|
||||
: '/uploads/dummy_Image_4abc3f04dd.webp'
|
||||
},
|
||||
|
||||
isMobile: (state) => state.screenWidth < 768,
|
||||
|
||||
getPageByLink: (state) => (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 ?? [];
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
getProjectByLink: (state) => (link: string) =>
|
||||
state.projects.find(project => project.link === link),
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleMenu() {
|
||||
this.menuOpen = !this.menuOpen
|
||||
},
|
||||
|
||||
closeMenu() {
|
||||
this.menuOpen = false
|
||||
},
|
||||
|
||||
toggleContactBubble() {
|
||||
this.contactBoxOpen = !this.contactBoxOpen
|
||||
},
|
||||
|
||||
setScrollPosition(pos: number) {
|
||||
this.scrollPosition = pos
|
||||
},
|
||||
|
||||
setScreenWidth(width: number) {
|
||||
this.screenWidth = width
|
||||
},
|
||||
@ -143,7 +140,7 @@ export const useMainStore = defineStore('main', {
|
||||
const { public: cfg } = useRuntimeConfig()
|
||||
|
||||
try {
|
||||
const [companyRes, pagesRes, customersRes] = await Promise.all([
|
||||
const [companyRes, pagesRes, customersRes, projectsRes] = await Promise.all([
|
||||
$fetch(`${cfg.cmsBaseUrl}/api/companyinfo?populate=*`, {
|
||||
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
|
||||
}),
|
||||
@ -153,12 +150,13 @@ export const useMainStore = defineStore('main', {
|
||||
$fetch(`${cfg.cmsBaseUrl}/api/customers?populate=*`, {
|
||||
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
|
||||
}),
|
||||
$fetch(`${cfg.cmsBaseUrl}/api/references?populate=projectImages,Technologien,customer&sort=launchDate:desc`, {
|
||||
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
|
||||
}),
|
||||
])
|
||||
|
||||
// CompanyInfo (Single Type)
|
||||
this.companyinfo = companyRes.data?.attributes ?? companyRes
|
||||
|
||||
// Pages
|
||||
this.pages = pagesRes.data.map((item: any) => {
|
||||
const a = item.attributes
|
||||
return {
|
||||
@ -174,7 +172,7 @@ export const useMainStore = defineStore('main', {
|
||||
SEO: a.SEO
|
||||
? {
|
||||
pageTitle: a.SEO.pageTitle,
|
||||
seoDescription: a.SEO.seoDesicription, // Fehler absichtlich
|
||||
seoDescription: a.SEO.seoDesicription, // absichtlicher Fehler
|
||||
seoKeywords: a.SEO.seoKeywords,
|
||||
type: a.SEO.type,
|
||||
seoImage: a.SEO.seoImage?.data
|
||||
@ -202,7 +200,6 @@ export const useMainStore = defineStore('main', {
|
||||
}
|
||||
})
|
||||
|
||||
// Customers
|
||||
this.customers = customersRes.data.map((item: any) => {
|
||||
const a = item.attributes
|
||||
return {
|
||||
@ -211,22 +208,34 @@ export const useMainStore = defineStore('main', {
|
||||
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,
|
||||
})) ?? [],
|
||||
})),
|
||||
projects: [], // Wird durch references geladen
|
||||
}
|
||||
})
|
||||
|
||||
this.projects = projectsRes.data.map((item: any) => {
|
||||
const a = item.attributes
|
||||
return {
|
||||
id: item.id,
|
||||
projectTitle: a.projectTitle,
|
||||
launchDate: a.launchDate,
|
||||
projectDescription: a.projectDescription,
|
||||
link: a.link,
|
||||
webpage: a.webpage,
|
||||
technologies: a.Technologien?.data?.map((t: any) => ({
|
||||
titel: t.attributes.titel,
|
||||
icon: t.attributes.icon,
|
||||
})) ?? [],
|
||||
projectImages: a.projectImages?.data?.map((img: any) => ({
|
||||
url: img.attributes.url,
|
||||
alternativeText: img.attributes.alternativeText,
|
||||
})) ?? [],
|
||||
customer: a.customer?.data
|
||||
? {
|
||||
id: a.customer.data.id,
|
||||
company: a.customer.data.attributes.company,
|
||||
city: a.customer.data.attributes.city,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user