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%
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
// +++++++++++++++++

View File

@ -1,5 +1,5 @@
<template>
<nav v-if="breadcrumbs && breadcrumbs.length" class="breadcrumbs" aria-label="Breadcrumb">
<nav v-if="breadcrumbs.length" class="breadcrumbs" aria-label="Breadcrumb">
<ul>
<li>
<router-link to="/" aria-label="Startseite">
@ -10,58 +10,98 @@
</router-link>
</li>
<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 }}
</router-link>
<span v-else>{{ crumb.label }}</span>
<span v-else :title="crumb.labelFull">{{ crumb.label }}</span>
</li>
</ul>
</nav>
</template>
<script>
<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
}
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]
}
const pathArray = pathWithoutLang.split('/').filter(p => p)
let path = ''
return pathArray.map(segment => {
return segments.map(segment => {
path += '/' + segment
const { label, labelFull } = formatLabel(segment)
return {
label: this.formatLabel(segment),
to: `/${locale}${path}` // Sprachprefix im Link wieder einfügen
label,
labelFull,
to: buildUrl(loc, path)
}
})
}
},
methods: {
formatLabel(segment) {
return segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ')
}
}
}
})
</script>
<style lang="sass">
<style lang="sass" scoped>
.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
@ -115,4 +155,3 @@
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 }
})
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

View File

@ -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)

View File

@ -24,7 +24,7 @@ const screenWidth = computed(() => mainStore.screenWidth)
height: auto
z-index: 12
&.mobile
top: 45vh
top: 20vh
</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
width: 100vw
color: white
z-index: 10
z-index: 9
height: auto
min-height: 120px
margin-top: 100px

View File

@ -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
}
}

View File

@ -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',

View File

@ -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!"
}
}
}
}

View File

@ -9,30 +9,30 @@
"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",
"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",
"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",
"privacyInfotextLinkText": "Information about data protection",
"validation": {
"nameRequired": "Name is a required field.",
"emailOrPhoneRequired": "Please enter either an email address or a phone number.",
@ -40,63 +40,96 @@
"invalidPhone": "Please enter a valid phone number."
},
"successMessage": "Your message has been sent successfully.",
"errorMessage": "There is currently a problem with the internet connection!",
"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 shortly...",
"salutation": "Your digimedialoop Team"
"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 custom web design and professional web development",
"h2": "Modular websites using the latest technologies",
"h3": "Making your website fast, efficient and future-proof!"
"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": "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.",
"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'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!"
"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": "Start using your websites full potential!",
"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, compelling design and optimized user experience, your website becomes a lead machine."
"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 your customer relationships! With valuable content, exclusive offers and interactive features, your target group remains active and engaged."
"text": "Strengthen relationships with your customers! With valuable content, exclusive offers, and interactive features, your audience stays 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."
"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! With clear information and digital processes on your website, you save time and costs while easing the workload for your team."
"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 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."
"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, well take your business to the next level!",
"title": "Together, we 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"
"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"
}
}
}
}

View File

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

View File

@ -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
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>
<div class="container-10">
<h1>{{ $t('references') }}</h1>
<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>

View File

@ -1,15 +1,243 @@
<template>
<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('services') }}</h1>
<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>

View File

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

View File

@ -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) => ({
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,
}
})