This commit is contained in:
Sabrina Hennrich 2025-05-25 21:45:54 +02:00
parent 3e302c5449
commit f929763892
19 changed files with 1220 additions and 279 deletions

View File

@ -110,6 +110,14 @@ body
width: 80% width: 80%
margin: auto 10% margin: auto 10%
.container-15
width: 70%
margin: auto 15%
.container-20
width: 60%
margin: auto 20%
.fade-enter-active, .fade-leave-active .fade-enter-active, .fade-leave-active
transition: opacity 1.2s ease transition: opacity 1.2s ease
@ -155,7 +163,22 @@ body
transform: translateY(200px) transform: translateY(200px)
opacity: 0 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
// +++++++++++++++++ // +++++++++++++++++

View File

@ -1,118 +1,157 @@
<template> <template>
<nav v-if="breadcrumbs && breadcrumbs.length" class="breadcrumbs" aria-label="Breadcrumb"> <nav v-if="breadcrumbs.length" class="breadcrumbs" aria-label="Breadcrumb">
<ul> <ul>
<li> <li>
<router-link to="/" aria-label="Startseite"> <router-link to="/" aria-label="Startseite">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" aria-hidden="true" focusable="false"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" aria-hidden="true" focusable="false">
<title>Startseite</title> <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"/> <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> </svg>
</router-link> </router-link>
</li> </li>
<li v-for="(crumb, index) in breadcrumbs" :key="index"> <li v-for="(crumb, index) in breadcrumbs" :key="index">
<router-link v-if="index < breadcrumbs.length - 1" :to="crumb.to"> <router-link v-if="index < breadcrumbs.length - 1" :to="crumb.to" :title="crumb.labelFull">
{{ crumb.label }} {{ crumb.label }}
</router-link> </router-link>
<span v-else>{{ crumb.label }}</span> <span v-else :title="crumb.labelFull">{{ crumb.label }}</span>
</li> </li>
</ul> </ul>
</nav> </nav>
</template> </template>
<script>
import { useI18n } from 'vue-i18n'
export default { <script setup lang="ts">
name: 'Breadcrumb', import { computed } from 'vue'
computed: { import { useI18n } from 'vue-i18n'
breadcrumbs() { import { useRoute } from 'vue-router'
const locale = this.$i18n.locale // aktives Sprachpräfix (z.B. "en", "de", etc.) import { i18nPages } from '~/i18n/i18n-pages'
const pathWithoutLang = this.$route.path.replace(`/${locale}`, '') // Sprachprefix entfernen
const pathArray = pathWithoutLang.split('/').filter(p => p) interface Breadcrumb {
let path = '' label: string
return pathArray.map(segment => { labelFull: string
path += '/' + segment to: string
return { }
label: this.formatLabel(segment),
to: `/${locale}${path}` // Sprachprefix im Link wieder einfügen 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) + '...'
methods: {
formatLabel(segment) {
return segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ')
}
}
} }
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> </script>
<style lang="sass"> <style lang="sass" scoped>
.breadcrumbs .breadcrumbs
position: fixed position: fixed
top: 22vh top: 22vh
left: 0 left: 0
text-align: left text-align: left
padding: 1rem .25rem 1rem .5rem padding: 1rem .25rem 1rem .5rem
//min-width: 200px background-color: rgba(white, .98)
background-color: rgba(white, .98) border: 1px solid darken($lightgrey, 5%)
border: 1px solid darken($lightgrey, 5%) writing-mode: vertical-rl
writing-mode: vertical-rl transform: rotate(180deg)
transform: rotate(180deg) border-top-left-radius: .8rem
//border-bottom-left-radius: .65rem border-bottom-left-radius: .8rem
border-top-left-radius: .8rem text-transform: uppercase
border-bottom-left-radius: .8rem letter-spacing: .05rem
text-transform: uppercase z-index: 22
letter-spacing: .05rem ul
z-index: 22 display: flex
ul flex-wrap: wrap
list-style: none
padding: 0
margin: 0
gap: 0.5rem
cursor: pointer
li
display: flex display: flex
flex-wrap: wrap align-items: center
list-style: none color: darken($primaryColor, 10%)
padding: 0 font-size: .7rem
margin: 0
gap: 0.5rem
cursor: pointer
li
display: flex
align-items: center
color: darken($primaryColor, 10%)
font-size: .7rem
svg svg
width: .8rem width: .8rem
height: .8rem height: .8rem
transform: rotate(90deg) transform: rotate(90deg)
margin-bottom: .35rem margin-bottom: .35rem
transition: .3s transition: .3s
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
path
fill: darken($lightgrey, 20%)
&:hover &:hover
transform: scale(1.3) rotate(90deg) transform: scale(1.2)
path
fill: darken($lightgrey, 30%)
&::after span
content: '>' font-weight: bold
margin: 0 0.5rem </style>
color: $pink
&:last-child::after
content: ''
a
color: #007BFF
text-decoration: none
&:hover
transform: scale(1.2)
span
font-weight: bold
</style>

View 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>

View File

@ -38,7 +38,7 @@
afterImage: { type: String, required: true } afterImage: { type: String, required: true }
}) })
const sliderValue = ref(60) // Startposition in der Mitte const sliderValue = ref(80) // Startposition in der Mitte
</script> </script>
<style lang="sass"> <style lang="sass">
@ -92,7 +92,7 @@
width: 20px // Breite des Schiebereglers width: 20px // Breite des Schiebereglers
height: 20px // Höhe des Schiebereglers height: 20px // Höhe des Schiebereglers
border-radius: 50% border-radius: 50%
background: $pink background: $primaryColor
border: 1px solid #fff border: 1px solid #fff
cursor: pointer cursor: pointer
z-index: 15 z-index: 15

View File

@ -21,7 +21,7 @@
<h2 class="pt-4 pb-3">{{ title }}</h2> <h2 class="pt-4 pb-3">{{ title }}</h2>
<div class="marquee"> <div class="marquee">
<div class="marquee-track"> <div class="marquee-track" :style="`animation-duration: ${speed}s`">
<ul class="marquee-list"> <ul class="marquee-list">
<li <li
v-for="(item, index) in items" v-for="(item, index) in items"
@ -31,10 +31,11 @@
<NuxtLink v-if="item.link" :to="item.link" class="custLogoLink"> <NuxtLink v-if="item.link" :to="item.link" class="custLogoLink">
<NuxtImg <NuxtImg
provider="strapi" provider="strapi"
:src="item.logo.url" :src="item.image.url"
:alt="item.logo.alternativeText || 'Logo'" :alt="item.image.alternativeText || item.text || 'Image'"
:title="item.text"
width="250" width="250"
height="50" :height="logoHeight"
format="webp" format="webp"
loading="lazy" loading="lazy"
class="custLogo" class="custLogo"
@ -43,13 +44,15 @@
<NuxtImg <NuxtImg
v-else v-else
provider="strapi" provider="strapi"
:src="item.logo.url" :src="item.image.url"
:alt="item.logo.alternativeText || 'Logo'" :alt="item.image.alternativeText || item.text || 'Image'"
:title="item.text"
width="250" width="250"
height="50" height="50"
format="webp" format="webp"
loading="lazy" loading="lazy"
class="custLogo" :class="['custLogo', { greyscale: greyscale }]"
/> />
</li> </li>
</ul> </ul>
@ -86,6 +89,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl) const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl)
@ -94,12 +98,26 @@ const props = defineProps({
items: { items: {
type: Array, type: Array,
required: true, required: true,
default: () => [],
}, },
title: { title: {
type: String, type: String,
default: '', 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> </script>
<style lang="sass" scoped> <style lang="sass" scoped>
@ -144,9 +162,12 @@ const props = defineProps({
.custLogo .custLogo
width: auto width: auto
max-width: 250px max-width: 250px
height: 50px transition: filter 0.3s ease, transform .5s ease
&:hover
transform: scale(1.1)
.greyscale
filter: grayscale(100%) filter: grayscale(100%)
transition: filter 0.3s ease
&:hover &:hover
filter: grayscale(0) filter: grayscale(0)

View File

@ -24,7 +24,7 @@ const screenWidth = computed(() => mainStore.screenWidth)
height: auto height: auto
z-index: 12 z-index: 12
&.mobile &.mobile
top: 45vh top: 20vh
</style> </style>

View 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>

View File

@ -158,7 +158,7 @@ footer
position: relative position: relative
width: 100vw width: 100vw
color: white color: white
z-index: 10 z-index: 9
height: auto height: auto
min-height: 120px min-height: 120px
margin-top: 100px margin-top: 100px

View File

@ -1,4 +1,3 @@
// composables/useI18nPages.ts
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { i18nPages } from '@/i18n/i18n-pages' import { i18nPages } from '@/i18n/i18n-pages'
@ -6,10 +5,23 @@ export function useI18nPages () {
const { locale } = useI18n() const { locale } = useI18n()
const getRoute = (key: keyof typeof i18nPages) => { 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 { return {
getRoute getRoute,
getProjectLink
} }
} }

View File

@ -39,6 +39,16 @@ export const i18nPages = {
es: '/aviso-legal', es: '/aviso-legal',
tr: '/künye' 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: { privacy: {
de: '/datenschutz', de: '/datenschutz',
en: '/privacy', en: '/privacy',

View File

@ -4,6 +4,7 @@
"services": "Leistungen", "services": "Leistungen",
"contact": "Kontakt", "contact": "Kontakt",
"references": "Referenzen", "references": "Referenzen",
"referenceoverview": "Referenzübersicht",
"imprint": "Impressum", "imprint": "Impressum",
"privacy": "Datenschutz", "privacy": "Datenschutz",
"privacyPolicy": "Datenschutzerklärung", "privacyPolicy": "Datenschutzerklärung",
@ -130,8 +131,48 @@
"title": "Du bist Grafikdesigner oder Mediengestalter?", "title": "Du bist Grafikdesigner oder Mediengestalter?",
"button": "Hier unser Angebot für Dich" "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!"
}
}
} }
} }

View File

@ -9,94 +9,127 @@
"privacyPolicy": "Privacy Policy", "privacyPolicy": "Privacy Policy",
"termsOfService": "Terms of Service", "termsOfService": "Terms of Service",
"terms": "Terms", "terms": "Terms",
"faq": "FAQ", "faq": "Frequently Asked Questions",
"magazin": "Insights", "magazin": "Knowledge",
"accessability": "Accessibility", "accessability": "Accessibility",
"accessibilitySettings": "Accessibility Settings", "accessibilitySettings": "Accessibility Settings",
"changeFontSize": "Increase text size", "changeFontSize": "Increase Text Size",
"greyscale": "Greyscale", "greyscale": "Grayscale",
"increaseContrast": "Increase contrast", "increaseContrast": "Increase Contrast",
"borderFocus": "Enable focus highlight", "borderFocus": "Enable Focus",
"hideImages": "Hide images", "hideImages": "Hide Images",
"showLinks": "Highlight links", "showLinks": "Highlight Links",
"infoAccessibility": "Information about the accessibility of our site", "infoAccessibility": "Information about accessibility on our site",
"importantLinks": "Important Links", "importantLinks": "Important Links",
"contactForm": { "contactForm": {
"yourcontact2us": "Get in touch with us!", "yourcontact2us": "Your contact to us!",
"ourOffice": "Our Office Address", "ourOffice": "Our office address",
"yourcontactperson": "Your Contact Person", "yourcontactperson": "Your contact person",
"name": "Name", "name": "Name",
"email": "Email", "email": "Email",
"phone": "Phone", "phone": "Phone",
"message": "Message", "message": "Message",
"company": "Company", "company": "Company",
"sendMessage": "Send Message", "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.", "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", "privacyInfotextLinkText": "Information about data protection",
"validation": { "validation": {
"nameRequired": "Name is a required field.", "nameRequired": "Name is a required field.",
"emailOrPhoneRequired": "Please enter either an email address or a phone number.", "emailOrPhoneRequired": "Please enter either an email address or a phone number.",
"invalidEmail": "Please enter a valid email address.", "invalidEmail": "Please enter a valid email address.",
"invalidPhone": "Please enter a valid phone number." "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, youll 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 websites 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."
}, },
"item2": { "successMessage": "Your message has been sent successfully.",
"title": "Retain customers and members", "errorMessage": "Unfortunately, there is currently an error with the internet connection!",
"text": "Strengthen your customer relationships! With valuable content, exclusive offers and interactive features, your target group remains active and engaged." "confirmation": {
}, "thx": "Thank you for your message!",
"item3": { "info": "We will get back to you promptly...",
"title": "Attract and inspire new employees", "salutation": "Your digimedialoop team"
"text": "Find the right talent! An authentic career page with clear benefits makes your company irresistible to applicants." }
}, },
"item4": { "faqBox": {
"title": "Reduce administrative effort", "questions": "Questions?",
"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." "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 websites 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 clients relaunch process.",
"text": "We place great emphasis on a clean design that aligns with users mental models so visitors always find exactly what theyre looking for, right where they expect it."
},
"finalCall": {
"title": "Together, well take your business to the next level!",
"button": "Contact us!"
},
"marqueeBanner": {
"title": "These companies trust us"
},
"faqArea": {
"headline": "Here youll find answers to frequently asked questions (FAQs) about website creation with digimedialoop"
}
} }
} }

View File

@ -158,6 +158,23 @@ main
border: none border: none
padding: .4rem .8rem padding: .4rem .8rem
margin: 0 0 1rem 0 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 section
margin-bottom: 5vh margin-bottom: 5vh
position: relative position: relative

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="homePage"> <div class="homePage">
<section class="heroBox" aria-labelledby="hero-heading"> <section class="heroBox_service" aria-labelledby="hero-heading">
<NuxtImg <NuxtImg
provider="strapi" provider="strapi"
src="/uploads/large_DML_Home_Hero_4f27bc7f8e.webp" 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> @click.prevent="toggleContactBubble">{{ $t('pages.home.finalCall.button') }}</button>
</div> </div>
</section> </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')" /> <FAQArea page-link="/" :headline="$t('pages.home.faqArea.headline')" />
</div> </div>
@ -168,13 +168,14 @@ const waveHeight = computed(() => (screenWidth.value / 25).toFixed(0));
const logoItems = computed(() => { const logoItems = computed(() => {
return customers.value.map(customer => ({ return customers.value.map(customer => ({
company: customer.company || '', text: customer.company || '',
logo: { image: {
url: customer.logo?.url || '', url: customer.logo?.url || '',
alternativeText: customer.company || '' alternativeText: customer.logo?.alternativeText || customer.company || ''
} }
})) }))
}) })
const canDoItems = [ const canDoItems = [
{ {
img: '/uploads/website_Erfolg_Marketing_3c36a43ba5.png', img: '/uploads/website_Erfolg_Marketing_3c36a43ba5.png',
@ -208,7 +209,7 @@ const canDoItems = [
<style lang="sass"> <style lang="sass">
.homePage .homePage
.heroBox .heroBox_service
position: relative position: relative
min-height: 35rem min-height: 35rem
height: 70vh height: 70vh
@ -267,22 +268,7 @@ const canDoItems = [
.container .container
z-index: 2 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 .webStrategy
padding: 4rem 0 3.5rem 0 padding: 4rem 0 3.5rem 0
@ -349,8 +335,11 @@ const canDoItems = [
max-width: 50% max-width: 50%
.homeImageTop .homeImageTop
margin: 4.5rem 0 8vh 3rem !important margin: 4.5rem 0 8vh 3rem !important
.compBox .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 padding: 5rem 0 3rem 0
h2 h2
font-family: 'Comfortaa' font-family: 'Comfortaa'

251
pages/projekt/[link].vue Normal file
View 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>

View File

@ -1,15 +1,183 @@
<template> <template>
<div class="container-10"> <div>
<h1>{{ $t('references') }}</h1> <section class="topSpace">
</div> <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> </template>
<script setup> <script setup>
definePageMeta({ import { useMainStore } from '@/stores/main'
layout: 'default' 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> </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
</style> 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>

View File

@ -1,15 +1,243 @@
<template> <template>
<div class="container-10"> <div>
<h1>{{ $t('services') }}</h1> <section class="heroBox" :aria-label="$t('pages.services.hero.ariaLabel')">
</div> <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> </template>
<script setup> <script setup>
definePageMeta({ import { storeToRefs } from 'pinia';
layout: 'default' 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> </script>
<style lang="sass"> <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> </style>

View File

@ -214,7 +214,7 @@ const navigateTo = useRouter().push;
@media(max-width: $breakPointSM) @media(max-width: $breakPointSM)
padding-left: 0 padding-left: 0
h2 h2
color: darken($lightgrey, 50%) color: darken($lightgrey, 30%)
font-size: .9rem font-size: .9rem
margin-bottom: 1rem margin-bottom: 1rem
h3 h3

View File

@ -26,7 +26,7 @@ interface CompanyInfo {
interface SEO { interface SEO {
pageTitle: string pageTitle: string
seoDescription: string // Achtung: Schreibfehler wird so übernommen seoDescription: string
seoKeywords: string seoKeywords: string
type: string type: string
seoImage?: CompanyLogo | null seoImage?: CompanyLogo | null
@ -62,6 +62,11 @@ interface CustomerProject {
webpage?: string webpage?: string
technologies: { titel: string; icon?: string }[] technologies: { titel: string; icon?: string }[]
projectImages: CompanyLogo[] projectImages: CompanyLogo[]
customer?: {
id: number
company: string
city: string
} | null
} }
interface Customer { interface Customer {
@ -82,6 +87,7 @@ export const useMainStore = defineStore('main', {
companyinfo: null as CompanyInfo | null, companyinfo: null as CompanyInfo | null,
pages: [] as Page[], pages: [] as Page[],
customers: [] as Customer[], customers: [] as Customer[],
projects: [] as CustomerProject[],
dataFetched: false, dataFetched: false,
loading: false, loading: false,
error: null as { message: string; stack?: string } | null, error: null as { message: string; stack?: string } | null,
@ -95,43 +101,34 @@ 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,
getPageByLink: (state) => (link: string) => getPageByLink: (state) => (link: string) =>
state.pages.find((p) => p.pageLink === link), state.pages.find((p) => p.pageLink === link),
getCustomerById: (state) => (id: number) => getCustomerById: (state) => (id: number) =>
state.customers.find((c) => c.id === id), state.customers.find((c) => c.id === id),
getFaqsByPageId: (state) => (pageId: number) => getFaqsByPageId: (state) => (pageId: number) =>
state.pages.find((p) => p.id === pageId)?.faqs ?? [], state.pages.find((p) => p.id === pageId)?.faqs ?? [],
getFaqsByPageLink: (state) => (link: string) => { getFaqsByPageLink: (state) => (link: string) => {
const page = state.pages.find(p => p.pageLink === link); const page = state.pages.find(p => p.pageLink === link);
return page?.faqs ?? []; return page?.faqs ?? [];
} },
getProjectByLink: (state) => (link: string) =>
state.projects.find(project => project.link === link),
}, },
actions: { actions: {
toggleMenu() { toggleMenu() {
this.menuOpen = !this.menuOpen this.menuOpen = !this.menuOpen
}, },
closeMenu() { closeMenu() {
this.menuOpen = false this.menuOpen = false
}, },
toggleContactBubble() { toggleContactBubble() {
this.contactBoxOpen = !this.contactBoxOpen this.contactBoxOpen = !this.contactBoxOpen
}, },
setScrollPosition(pos: number) { setScrollPosition(pos: number) {
this.scrollPosition = pos this.scrollPosition = pos
}, },
setScreenWidth(width: number) { setScreenWidth(width: number) {
this.screenWidth = width this.screenWidth = width
}, },
@ -143,7 +140,7 @@ export const useMainStore = defineStore('main', {
const { public: cfg } = useRuntimeConfig() const { public: cfg } = useRuntimeConfig()
try { try {
const [companyRes, pagesRes, customersRes] = await Promise.all([ const [companyRes, pagesRes, customersRes, projectsRes] = await Promise.all([
$fetch(`${cfg.cmsBaseUrl}/api/companyinfo?populate=*`, { $fetch(`${cfg.cmsBaseUrl}/api/companyinfo?populate=*`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` }, headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}), }),
@ -153,12 +150,13 @@ export const useMainStore = defineStore('main', {
$fetch(`${cfg.cmsBaseUrl}/api/customers?populate=*`, { $fetch(`${cfg.cmsBaseUrl}/api/customers?populate=*`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` }, 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 this.companyinfo = companyRes.data?.attributes ?? companyRes
// Pages
this.pages = pagesRes.data.map((item: any) => { this.pages = pagesRes.data.map((item: any) => {
const a = item.attributes const a = item.attributes
return { return {
@ -174,7 +172,7 @@ export const useMainStore = defineStore('main', {
SEO: a.SEO SEO: a.SEO
? { ? {
pageTitle: a.SEO.pageTitle, pageTitle: a.SEO.pageTitle,
seoDescription: a.SEO.seoDesicription, // Fehler absichtlich seoDescription: a.SEO.seoDesicription, // absichtlicher Fehler
seoKeywords: a.SEO.seoKeywords, seoKeywords: a.SEO.seoKeywords,
type: a.SEO.type, type: a.SEO.type,
seoImage: a.SEO.seoImage?.data seoImage: a.SEO.seoImage?.data
@ -202,7 +200,6 @@ export const useMainStore = defineStore('main', {
} }
}) })
// Customers
this.customers = customersRes.data.map((item: any) => { this.customers = customersRes.data.map((item: any) => {
const a = item.attributes const a = item.attributes
return { return {
@ -211,22 +208,34 @@ export const useMainStore = defineStore('main', {
city: a.city, city: a.city,
logo: a.logo?.data?.attributes ?? null, logo: a.logo?.data?.attributes ?? null,
invertLogo: a.invertLogo?.data?.attributes ?? null, invertLogo: a.invertLogo?.data?.attributes ?? null,
projects: (a.projects?.data ?? []).map((p: any) => ({ projects: [], // Wird durch references geladen
id: p.id, }
projectTitle: p.attributes.projectTitle, })
projectImages: p.attributes.projectImages?.data?.map((img: any) => ({
url: img.attributes.url, this.projects = projectsRes.data.map((item: any) => {
alternativeText: img.attributes.alternativeText, const a = item.attributes
})) ?? [], return {
launchDate: p.attributes.launchDate, id: item.id,
projectDescription: p.attributes.projectDescription, projectTitle: a.projectTitle,
link: p.attributes.link, launchDate: a.launchDate,
webpage: p.attributes.webpage, projectDescription: a.projectDescription,
technologies: p.attributes.Technologien?.data?.map((t: any) => ({ link: a.link,
titel: t.attributes.titel, webpage: a.webpage,
icon: t.attributes.icon, 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,
} }
}) })