ready check

This commit is contained in:
Sabrina Hennrich 2025-05-26 16:32:19 +02:00
parent f929763892
commit aa1203bff8
23 changed files with 1875 additions and 384 deletions

View File

@ -0,0 +1,66 @@
<template>
<transition name="fade">
<button class="back-to-top" v-show="isVisible" @click="scrollToTop" aria-label="Zurück nach oben">
<span class="arrow-up"></span>
</button>
</transition>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const isVisible = ref(false)
const toggleVisibility = () => {
isVisible.value = window.scrollY > 100
}
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
onMounted(() => {
window.addEventListener('scroll', toggleVisibility)
})
onUnmounted(() => {
window.removeEventListener('scroll', toggleVisibility)
})
</script>
<style lang="sass">
.back-to-top
position: fixed
bottom: 2rem
left: 2rem
background-color: rgba($darkgrey, .95)
border: 1px solid white
z-index: 100
border-radius: 50%
width: 3rem
height: 3rem
display: flex
align-items: center
justify-content: center
cursor: pointer
transition: opacity 0.4s ease-in-out
opacity: 0.8
&:hover
opacity: 1
.arrow-up
width: 0
height: 0
border-left: 8px solid transparent
border-right: 8px solid transparent
border-bottom: 12px solid white
// Transition für den Button
.fade-enter-active, .fade-leave-active
transition: opacity 0.4s ease-in-out
.fade-enter-from, .fade-leave-to
opacity: 0
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="languageBox" @click="toggleOpen"> <div class="languageBox" v-if="locales.length >=2" @click="toggleOpen">
<div v-if="!open" class="current">{{ currentLanguage }}</div> <div v-if="!open" class="current">{{ currentLanguage }}</div>
<transition name="slide"> <transition name="slide">

View File

@ -0,0 +1,50 @@
<template>
<section class="topSpace" v-if="currentPage">
<div v-if="currentPage.pageSections[0].sectionText">
<div class="container content" v-html="htmlContent(currentPage.pageSections[0].sectionText)"></div>
</div>
</section>
<section class="topSpace" v-else>
<h1>Seite nicht gefunden</h1>
<p>Die angeforderte Seite existiert nicht.</p>
</section>
</template>
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useMainStore } from '@/stores/main';
import { useHtmlConverter } from '@/composables/useHTMLConverter';
const route = useRoute();
const mainStore = useMainStore();
const { convertToHTML } = useHtmlConverter();
// Aktuelle Seite über Getter und Route
const currentPage = computed(() => mainStore.getPageByLink(route.path));
// HTML-Konvertierung
const htmlContent = (data) => convertToHTML(data);
</script>
<style lang="sass">
section:first-of-type::before
top: 0
left: 0
width: 10vw
.content
h2
font-size: 1.1rem
font-family: 'Mainfont-Bold'
margin: 1rem 0
h3
font-size: 1rem
font-family: 'Mainfont-Bold'
margin: .6rem 0
p
font-size: 1rem
margin: .5rem auto
</style>

View File

@ -0,0 +1,147 @@
<template>
<section class="recommendations">
<!-- Vor dem Container: Welle oben -->
<svg class="sectionWave wave-top" :style="`height: ${waveHeight};top:-${waveHeight-2}`" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#EEEBE5"></path>
</svg>
<div class="container">
<h2>Das sagen andere Designer und Kreative über digimedialoop</h2>
<div class="personBox" v-for="person, index in persons" :key="index" :class="person.active ? 'active' : ''" @click="setActive(index)">
<img :src="person.image" alt="">
<div class="infoBox">
<h3>{{ person.name }}</h3>
<q>{{ person.quote }}</q>
</div>
</div>
</div>
<!-- Nach dem Container: Spiegelwelle unten -->
<svg class="sectionWave wave-bottom" :style="`height: ${waveHeight};bottom:-${waveHeight-2}`" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#EEEBE5"></path>
</svg>
</section>
</template>
<script setup>
import { ref } from 'vue'
import { useMainStore } from '@/stores/main';
const mainStore = useMainStore();
const screenWidth = computed(() => mainStore.screenWidth);
const waveHeight = computed(() => (screenWidth.value / 25).toFixed(0));
const persons = ref([
{
name: 'Nadine Mattern',
quote: 'Fachlich wirklich kompetent, die Ruhe selbst (auch bei knappen Deadlines) und absolut angenehme und verlässliche Zusammenarbeit. Gerne jederzeit wieder für ein nächstes Projekt. Ich kann Sabrina wärmstens weiter empfehlen!',
image: 'https://strapi.digimedialoop.de/uploads/Nadine_Matterns_2b5c0500a3.jpeg',
active: true
},
{
name: 'Anna Liebel',
quote: 'Jeder Mediengestalter sollte einen guten Entwickler im Rücken haben. Sabrina ist meine Wahl, wenn es technisch knifflig wird.',
image: 'https://strapi.digimedialoop.de/uploads/Anna_Liebel_8fb67e4a86.jpg',
active: false
},
])
// Methode zum Setzen der aktiven Person mit Verzögerung
const setActive = (index) => {
// Alle Personen auf inactive setzen
persons.value.forEach((person) => {
person.active = false;
});
// Nach 0,5 Sekunden die gewählte Person aktiv setzen
setTimeout(() => {
persons.value[index].active = true;
}, 200); // 500 Millisekunden warten
}
</script>
<style lang="sass">
.recommendations
background-color: $beige
padding: .5rem 0 6rem 0
margin: 6rem auto !important
position: relative
/* Welle oben */
.sectionWave.wave-top
position: absolute
left: 0
width: 100%
z-index:1
transform: scaley(-1) /* Spiegeln der oberen Welle */
/* Welle unten */
.sectionWave.wave-bottom
position: absolute
left: 0
width: 100%
z-index: 1
transform: scaleX(1) /* Die untere Welle wird nicht gespiegelt */
h2
font-size: 1.2rem
font-family: 'Mainfont-Bold'
color: darken($pink, 10%)
.personBox
display: inline-block
margin: 0 3rem 2rem 0
width: 100px
transition: .3s
position: relative
cursor: pointer
animation: bubble-wobble 12s infinite ease alternate, gradient-animation 15s infinite alternate ease-in-out
z-index: 3
.infoBox
display: none
img
width: 100%
border-radius: 50%
border: 6px solid rgba(white, .4)
z-index: 4
box-shadow: $innerShadow
&.active
width: 55%
max-width: 260px
transition: .8s
z-index: 5
animation: bubble-wobble 15s infinite ease alternate, gradient-animation 10s infinite alternate ease-in-out
.infoBox
display: block
position: absolute
top: 70%
left: 80%
transform: translateX(-50%)
background-color: rgba(white, .7)
width: 120%
max-width: 400px
padding: .5rem 1rem 1rem 1rem
border-radius: 1rem
z-index: 5
@media(max-width: $breakPointMD)
width: 150%
transform: translateX(-30%)
h3
font-size: 1.2rem
font-family: 'Mainfont-Bold'
margin: 0
color: darken($primaryColor, 15%)
text-transform: uppercase
q
font-size: .8rem
line-height: 1rem
font-family: 'Mainfont-Bold'
</style>

View File

@ -28,6 +28,16 @@ class="closer"
@click="toggleMenu" @click="toggleMenu"
@keydown.enter="toggleMenu"/> @keydown.enter="toggleMenu"/>
<nav v-if="isMenuOpen || screenWidth > 1350" aria-expanded="true"> <nav v-if="isMenuOpen || screenWidth > 1350" aria-expanded="true">
<div class="mobilNavLogo" v-if="false">
<NuxtImg
provider="strapi"
src="/uploads/DML_Logo_mint_negative_2024_9257db5430.svg"
alt="digimedialoop Logo"
width="120"
v-if="screenWidth < 1350"
/>
</div>
<NuxtLinkLocale <NuxtLinkLocale
v-for="link in navigationLinks" v-for="link in navigationLinks"
:key="link.routeKey" :key="link.routeKey"
@ -310,6 +320,7 @@ header
border-bottom-right-radius: 0 border-bottom-right-radius: 0
background: transparent background: transparent
border: 1px solid transparent border: 1px solid transparent
a a
font-size: 1rem font-size: 1rem
font-weight: bold font-weight: bold
@ -320,5 +331,4 @@ header
margin-bottom: .5rem margin-bottom: .5rem
width: 70% width: 70%
max-width: 200px max-width: 200px
</style> </style>

View File

@ -20,8 +20,14 @@ export function useI18nPages () {
return path.replace(':link', slug) return path.replace(':link', slug)
} }
const getArticleLink = (slug: string) => {
const path = getRoute('artikel___link')
return path.replace(':link', slug)
}
return { return {
getRoute, getRoute,
getProjectLink getProjectLink,
getArticleLink
} }
} }

View File

@ -49,6 +49,14 @@ export const i18nPages = {
tr: '/projekt/:link' tr: '/projekt/:link'
} }
}, },
/*designer: {
de: '/webentwicklung-fuer-designer-und-mediengestalter',
en: '/web-development-for-designers-and-media-creators',
fr: '/developpement-web-pour-designers-et-createurs-de-medias',
it: '/sviluppo-web-per-designer-e-creativi-multimediali',
es: '/desarrollo-web-para-disenadores-y-creadores-de-medios',
tr: '/tasarimcilar-ve-medya-uzmanlari-icin-web-gelistirme'
},*/
privacy: { privacy: {
de: '/datenschutz', de: '/datenschutz',
en: '/privacy', en: '/privacy',
@ -72,5 +80,15 @@ export const i18nPages = {
it: '/magazine', it: '/magazine',
es: '/revista', es: '/revista',
tr: '/dergi' tr: '/dergi'
},
'artikel___link': {
paths: {
de: '/artikel/:link',
en: '/artikel/:link',
fr: '/artikel/:link',
it: '/artikel/:link',
es: '/artikel/:link',
tr: '/artikel/:link'
} }
},
} }

View File

@ -137,7 +137,7 @@
"ariaLabel": "Einleitungsbereich über digitale Lösungen", "ariaLabel": "Einleitungsbereich über digitale Lösungen",
"imageAlt": "Illustration von Programmierung am Ammersee", "imageAlt": "Illustration von Programmierung am Ammersee",
"headline1": "High-Performance-Webseiten", "headline1": "High-Performance-Webseiten",
"headline2": "mit moderner Headless-Architektur", "headline2": "mit Nuxt 3 + Strapi in moderner Headless-Architektur",
"headline3": "Schnell, effizient und leistungsstark!" "headline3": "Schnell, effizient und leistungsstark!"
}, },
"explain": { "explain": {
@ -172,6 +172,12 @@
"text": "Lassen Sie uns gemeinsam Ihre individuelle Website gestalten perfekt abgestimmt auf Ihre Bedürfnisse und Ziele.", "text": "Lassen Sie uns gemeinsam Ihre individuelle Website gestalten perfekt abgestimmt auf Ihre Bedürfnisse und Ziele.",
"button": "Jetzt unverbindliches Angebot anfordern!" "button": "Jetzt unverbindliches Angebot anfordern!"
} }
},
"magazin": {
"title": "Wissenswertes rund ums Thema Webseite",
"teaser1": "In unserem Wissensbereich zeigen wir, worauf es bei Suchmaschinen-Optimierung, Barrierefreiheit, Webdesign, Webperformance und Online-Marketing ankommt.",
"teaser2": "Entdecken Sie aktuelle Trends und praxisnahe Tipps, um Ihre Online-Präsenz gezielt zu stärken.",
"readmore": "Artikel lesen"
} }
} }

View File

@ -3,6 +3,7 @@
<div> <div>
<PageHeader /> <PageHeader />
<ContactForm /> <ContactForm />
<BackToTopBtn />
<main> <main>
<Breadcrumbs /> <Breadcrumbs />
<slot /> <slot />

View File

@ -1,5 +1,6 @@
import { defineNuxtConfig } from 'nuxt/config' import { defineNuxtConfig } from 'nuxt/config'
import { i18nPages } from './i18n/i18n-pages' import { i18nPages } from './i18n/i18n-pages'
import { getSitemapRoutes } from './server/utils/sitemapData'
export default defineNuxtConfig({ export default defineNuxtConfig({
app: { app: {
@ -42,6 +43,7 @@ export default defineNuxtConfig({
'@nuxt/eslint', '@nuxt/eslint',
'@pinia/nuxt', '@pinia/nuxt',
'@nuxtjs/i18n', '@nuxtjs/i18n',
'@nuxtjs/sitemap',
['@pinia/nuxt', { ['@pinia/nuxt', {
autoImports: [ autoImports: [
'defineStore', 'defineStore',
@ -50,6 +52,14 @@ export default defineNuxtConfig({
] ]
}], }],
], ],
sitemap: {
siteUrl: process.env.APP_URL || 'https://www.digimedialoop.de',
trailingSlash: false,
i18n: true,
async routes() {
return await getSitemapRoutes()
}
},
image: { image: {
debug: true, debug: true,
strapi: { strapi: {
@ -73,13 +83,14 @@ export default defineNuxtConfig({
i18n: { i18n: {
defaultLocale: 'de', defaultLocale: 'de',
strategy: 'prefix_except_default', strategy: 'prefix_except_default',
langDir: 'locales',
locales: [ locales: [
{ code: 'de', name: 'Deutsch', file: 'de.json' }, { code: 'de', name: 'Deutsch', file: 'de.json' },
{ code: 'en', name: 'English', file: 'en.json' }, /*{ code: 'en', name: 'English', file: 'en.json' },
{ code: 'es', name: 'Español', file: 'es.json' }, { code: 'es', name: 'Español', file: 'es.json' },
{ code: 'fr', name: 'Français', file: 'fr.json' }, { code: 'fr', name: 'Français', file: 'fr.json' },
{ code: 'it', name: 'Italiano', file: 'it.json' }, { code: 'it', name: 'Italiano', file: 'it.json' },
{ code: 'tr', name: 'Türkçe', file: 'tr.json' } { code: 'tr', name: 'Türkçe', file: 'tr.json' }*/
], ],
customRoutes: 'config', customRoutes: 'config',
pages: i18nPages, pages: i18nPages,

363
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@nuxt/scripts": "^0.11.6", "@nuxt/scripts": "^0.11.6",
"@nuxt/ui": "^3.0.2", "@nuxt/ui": "^3.0.2",
"@nuxtjs/i18n": "^9.5.3", "@nuxtjs/i18n": "^9.5.3",
"@nuxtjs/sitemap": "^7.3.0",
"@pinia/nuxt": "^0.11.0", "@pinia/nuxt": "^0.11.0",
"nitropack": "^2.11.9", "nitropack": "^2.11.9",
"nuxt": "^3.16.2", "nuxt": "^3.16.2",
@ -3427,6 +3428,151 @@
"url": "https://github.com/sponsors/bobbiegoede" "url": "https://github.com/sponsors/bobbiegoede"
} }
}, },
"node_modules/@nuxtjs/sitemap": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/@nuxtjs/sitemap/-/sitemap-7.3.0.tgz",
"integrity": "sha512-R3X5hqhWoMXIB4XWQmUuVSGmPFrKTFYFho7pk990si8iayKjSMbBCtTPtHdEscglZukKy4cL7V0WYdDFrUvV5w==",
"license": "MIT",
"dependencies": {
"@nuxt/devtools-kit": "^2.4.1",
"@nuxt/kit": "^3.17.3",
"chalk": "^5.4.1",
"defu": "^6.1.4",
"h3-compression": "^0.3.2",
"nuxt-site-config": "^3.2.0",
"ofetch": "^1.4.1",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"radix3": "^1.1.2",
"semver": "^7.7.2",
"sirv": "^3.0.1",
"ufo": "^1.6.1"
},
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/@nuxt/devtools-kit": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-2.4.1.tgz",
"integrity": "sha512-taA2Nm03JiV3I+SEYS/u1AfjvLm3V9PO8lh0xLsUk/2mlUnL6GZ9xLXrp8VRg11HHt7EPXERGQh8h4iSPU2bSQ==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.17.3",
"@nuxt/schema": "^3.17.3",
"execa": "^8.0.1"
},
"peerDependencies": {
"vite": ">=6.0"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/@nuxt/kit": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.17.4.tgz",
"integrity": "sha512-l+hY8sy2XFfg3PigZj+PTu6+KIJzmbACTRimn1ew/gtCz+F38f6KTF4sMRTN5CUxiB8TRENgEonASmkAWfpO9Q==",
"license": "MIT",
"dependencies": {
"c12": "^3.0.4",
"consola": "^3.4.2",
"defu": "^6.1.4",
"destr": "^2.0.5",
"errx": "^0.1.0",
"exsolve": "^1.0.5",
"ignore": "^7.0.4",
"jiti": "^2.4.2",
"klona": "^2.0.6",
"knitwork": "^1.2.0",
"mlly": "^1.7.4",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"semver": "^7.7.2",
"std-env": "^3.9.0",
"tinyglobby": "^0.2.13",
"ufo": "^1.6.1",
"unctx": "^2.4.1",
"unimport": "^5.0.1",
"untyped": "^2.0.0"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/@nuxt/schema": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.17.4.tgz",
"integrity": "sha512-bsfJdWjKNYLkVQt7Ykr9YsAql1u8Tuo6iecSUOltTIhsvAIYsknRFPHoNKNmaiv/L6FgCQgUgQppPTPUAXiJQQ==",
"license": "MIT",
"dependencies": {
"@vue/shared": "^3.5.14",
"consola": "^3.4.2",
"defu": "^6.1.4",
"pathe": "^2.0.3",
"std-env": "^3.9.0"
},
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/@vue/shared": {
"version": "3.5.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.15.tgz",
"integrity": "sha512-bKvgFJJL1ZX9KxMCTQY6xD9Dhe3nusd1OhyOb1cJYGqvAr0Vg8FIjHPMOEVbJ9GDT9HG+Bjdn4oS8ohKP8EvoA==",
"license": "MIT"
},
"node_modules/@nuxtjs/sitemap/node_modules/chalk": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/unimport": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.0.1.tgz",
"integrity": "sha512-1YWzPj6wYhtwHE+9LxRlyqP4DiRrhGfJxdtH475im8ktyZXO3jHj/3PZ97zDdvkYoovFdi0K4SKl3a7l92v3sQ==",
"license": "MIT",
"dependencies": {
"acorn": "^8.14.1",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"local-pkg": "^1.1.1",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"strip-literal": "^3.0.0",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.2",
"unplugin-utils": "^0.2.4"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/@oxc-parser/binding-darwin-arm64": { "node_modules/@oxc-parser/binding-darwin-arm64": {
"version": "0.61.2", "version": "0.61.2",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.61.2.tgz", "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.61.2.tgz",
@ -6650,16 +6796,16 @@
} }
}, },
"node_modules/c12": { "node_modules/c12": {
"version": "3.0.3", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.0.3.tgz", "resolved": "https://registry.npmjs.org/c12/-/c12-3.0.4.tgz",
"integrity": "sha512-uC3MacKBb0Z15o5QWCHvHWj5Zv34pGQj9P+iXKSpTuSGFS0KKhUWf4t9AJ+gWjYOdmWCPEGpEzm8sS0iqbpo1w==", "integrity": "sha512-t5FaZTYbbCtvxuZq9xxIruYydrAGsJ+8UdP0pZzMiK2xl/gNiSOy0OxhLzHUEEb0m1QXYqfzfvyIFEmz/g9lqg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chokidar": "^4.0.3", "chokidar": "^4.0.3",
"confbox": "^0.2.2", "confbox": "^0.2.2",
"defu": "^6.1.4", "defu": "^6.1.4",
"dotenv": "^16.4.7", "dotenv": "^16.5.0",
"exsolve": "^1.0.4", "exsolve": "^1.0.5",
"giget": "^2.0.0", "giget": "^2.0.0",
"jiti": "^2.4.2", "jiti": "^2.4.2",
"ohash": "^2.0.11", "ohash": "^2.0.11",
@ -9763,6 +9909,18 @@
"uncrypto": "^0.1.3" "uncrypto": "^0.1.3"
} }
}, },
"node_modules/h3-compression": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/h3-compression/-/h3-compression-0.3.2.tgz",
"integrity": "sha512-B+yCKyDRnO0BXSfjAP4tCXJgJwmnKp3GyH5Yh66mY9KuOCrrGQSPk/gBFG2TgH7OyB/6mvqNZ1X0XNVuy0qRsw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/codedredd"
},
"peerDependencies": {
"h3": "^1.6.0"
}
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -11952,6 +12110,180 @@
} }
} }
}, },
"node_modules/nuxt-site-config": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nuxt-site-config/-/nuxt-site-config-3.2.0.tgz",
"integrity": "sha512-o1LDV+eaiP0qgM97RxoX2ost3mzmNmg5D3BmiORXCD9lx9CR5OZKc7nXI0zGsASk3eSVj4iNp0ctyF6afPFTow==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.17.3",
"nuxt-site-config-kit": "3.2.0",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"sirv": "^3.0.1",
"site-config-stack": "3.2.0",
"ufo": "^1.6.1"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/nuxt-site-config-kit": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nuxt-site-config-kit/-/nuxt-site-config-kit-3.2.0.tgz",
"integrity": "sha512-kVBXljR7Py8mz5eL6ZysVMlPRwbVX1Tts66StQRwYSJL/srEL8kr/ZfLW6tQU7pDHihcPH3MDgid2gDTFMY3fg==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.17.3",
"pkg-types": "^2.1.0",
"site-config-stack": "3.2.0",
"std-env": "^3.9.0",
"ufo": "^1.6.1"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/nuxt-site-config-kit/node_modules/@nuxt/kit": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.17.4.tgz",
"integrity": "sha512-l+hY8sy2XFfg3PigZj+PTu6+KIJzmbACTRimn1ew/gtCz+F38f6KTF4sMRTN5CUxiB8TRENgEonASmkAWfpO9Q==",
"license": "MIT",
"dependencies": {
"c12": "^3.0.4",
"consola": "^3.4.2",
"defu": "^6.1.4",
"destr": "^2.0.5",
"errx": "^0.1.0",
"exsolve": "^1.0.5",
"ignore": "^7.0.4",
"jiti": "^2.4.2",
"klona": "^2.0.6",
"knitwork": "^1.2.0",
"mlly": "^1.7.4",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"semver": "^7.7.2",
"std-env": "^3.9.0",
"tinyglobby": "^0.2.13",
"ufo": "^1.6.1",
"unctx": "^2.4.1",
"unimport": "^5.0.1",
"untyped": "^2.0.0"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/nuxt-site-config-kit/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nuxt-site-config-kit/node_modules/unimport": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.0.1.tgz",
"integrity": "sha512-1YWzPj6wYhtwHE+9LxRlyqP4DiRrhGfJxdtH475im8ktyZXO3jHj/3PZ97zDdvkYoovFdi0K4SKl3a7l92v3sQ==",
"license": "MIT",
"dependencies": {
"acorn": "^8.14.1",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"local-pkg": "^1.1.1",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"strip-literal": "^3.0.0",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.2",
"unplugin-utils": "^0.2.4"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/nuxt-site-config/node_modules/@nuxt/kit": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.17.4.tgz",
"integrity": "sha512-l+hY8sy2XFfg3PigZj+PTu6+KIJzmbACTRimn1ew/gtCz+F38f6KTF4sMRTN5CUxiB8TRENgEonASmkAWfpO9Q==",
"license": "MIT",
"dependencies": {
"c12": "^3.0.4",
"consola": "^3.4.2",
"defu": "^6.1.4",
"destr": "^2.0.5",
"errx": "^0.1.0",
"exsolve": "^1.0.5",
"ignore": "^7.0.4",
"jiti": "^2.4.2",
"klona": "^2.0.6",
"knitwork": "^1.2.0",
"mlly": "^1.7.4",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"semver": "^7.7.2",
"std-env": "^3.9.0",
"tinyglobby": "^0.2.13",
"ufo": "^1.6.1",
"unctx": "^2.4.1",
"unimport": "^5.0.1",
"untyped": "^2.0.0"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/nuxt-site-config/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nuxt-site-config/node_modules/unimport": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.0.1.tgz",
"integrity": "sha512-1YWzPj6wYhtwHE+9LxRlyqP4DiRrhGfJxdtH475im8ktyZXO3jHj/3PZ97zDdvkYoovFdi0K4SKl3a7l92v3sQ==",
"license": "MIT",
"dependencies": {
"acorn": "^8.14.1",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"local-pkg": "^1.1.1",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"strip-literal": "^3.0.0",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.2",
"unplugin-utils": "^0.2.4"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/nuxt/node_modules/@oxc-parser/binding-darwin-arm64": { "node_modules/nuxt/node_modules/@oxc-parser/binding-darwin-arm64": {
"version": "0.67.0", "version": "0.67.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.67.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.67.0.tgz",
@ -14200,9 +14532,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.1", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@ -14517,6 +14849,21 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/site-config-stack": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/site-config-stack/-/site-config-stack-3.2.0.tgz",
"integrity": "sha512-YmHQr5nMfec5ZTtuxK52YVG8nj4DwI68rSsbznuurQ96bIh2bHf5mNPAZnAyDBPpTWRwmWAliWMRD6WFewWGdA==",
"license": "MIT",
"dependencies": {
"ufo": "^1.6.1"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
},
"peerDependencies": {
"vue": "^3"
}
},
"node_modules/slash": { "node_modules/slash": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",

View File

@ -21,6 +21,7 @@
"@nuxt/scripts": "^0.11.6", "@nuxt/scripts": "^0.11.6",
"@nuxt/ui": "^3.0.2", "@nuxt/ui": "^3.0.2",
"@nuxtjs/i18n": "^9.5.3", "@nuxtjs/i18n": "^9.5.3",
"@nuxtjs/sitemap": "^7.3.0",
"@pinia/nuxt": "^0.11.0", "@pinia/nuxt": "^0.11.0",
"nitropack": "^2.11.9", "nitropack": "^2.11.9",
"nuxt": "^3.16.2", "nuxt": "^3.16.2",

126
pages/artikel/[link].vue Normal file
View File

@ -0,0 +1,126 @@
<template>
<div class="article" v-if="article">
<SideBarNaviSlider link="/wissenswertes">
{{ $t('artikel.artikelUebersicht') /* Beispiel für i18n */ }}
</SideBarNaviSlider>
<section class="teaserBox topSpace">
<div class="container-10">
<p class="articleInfo">
<b>{{ $t('artikel.autor') }}</b> {{ author }} |
<b>{{ $t('artikel.aktualisiert') }}</b> {{ formattedDate }}
</p>
<NuxtImg
v-if="article.image?.url"
:src="article.image.url"
:alt="article.image.alternativeText || article.header"
class="img_detail"
width="350"
height="auto"
priority
provider="strapi"
:sizes="'(max-width: 600px) 90vw, 350px'"
role="img"
/>
<h1>{{ article.header }}</h1>
<p class="teaser">{{ article.teaser }}</p>
</div>
</section>
<section class="articleBox container">
<div v-html="htmlContent(article.content)" class="content"></div>
<button
@click.prevent="toggleContactBubble"
class="pinkBtn"
role="button"
aria-label="Kontakt aufnehmen"
>
{{ $t('kontakt.kontaktieren') }}
</button>
</section>
</div>
<div v-else class="container topSpace">
<p>{{ $t('artikel.ladenOderNichtGefunden') }}</p>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'
import { useHtmlConverter } from '@/composables/useHTMLConverter'
import SideBarNaviSlider from '@/components/SideBarNaviSlider.vue'
import { useI18n } from 'vue-i18n'
const route = useRoute()
const slug = route.params.link
console.log(slug)
const mainStore = useMainStore()
const { articles } = storeToRefs(mainStore)
const { t } = useI18n()
// Artikel suchen
const article = computed(() => {
if (!articles.value) return null
return articles.value.find(item => item.slug === slug) ?? null
})
const { convertToHTML } = useHtmlConverter()
const htmlContent = (content) => convertToHTML(content)
const toggleContactBubble = () => mainStore.toggleContactBubble()
// Beispiel Autor und formatierte Datum (kannst du anpassen)
const author = 'Sabrina Hennrich'
const formattedDate = computed(() => {
if (!article.value?.dateModified) return ''
const d = new Date(article.value.dateModified)
return d.toLocaleDateString('de-DE')
})
</script>
<style scoped lang="sass">
.article
margin-top: 2rem
.teaserBox
margin-bottom: 1.5rem !important
.teaser
font-family: 'Mainfont-Bold'
.articleInfo
font-size: 0.8rem
color: lighten(#333, 50%)
b
color: #e91e63
h1
font-size: 1.6rem
font-family: 'Mainfont-Bold'
margin-bottom: 1.5rem
line-height: 2.2rem
.img_detail
width: 100%
max-width: 350px
float: right
margin: 0 0 2rem 2rem
border-radius: 1rem
filter: grayscale(100%)
.articleBox
display: flex
flex-direction: column
align-items: flex-start
.pinkBtn
clear: both
display: block
width: max-content
</style>

393
pages/designer/index.vue Normal file
View File

@ -0,0 +1,393 @@
<template>
<div class="landing topSpace">
<section class="heroBox">
<div class="container">
<div class="contentBox">
<p class="supheadlinePink">Webentwicklung für Grafikdesigner und Mediengestalter</p>
<h1>Du hast ein geniales Design im Kopf ... </h1>
<h2>...doch im Pagebuilder lässt es sich nicht umsetzen?</h2>
<h3>Jou! Das kann richtig frustrierend sein - muss es aber nicht! <br>Gemeinsam setzen wir deine kreativen Ideen pixelgenau, performant und individuell um!</h3>
</div>
<div class="speechBox d-flex align-items-start">
<NuxtImg
class="profileImage"
src="/uploads/sabrinahennrich_0f07d46857.jpg"
provider="strapi"
alt="Sabrina Hennrich"
/>
<div class="bubble">
<p><b><span>Servus!</span> <br>Ich bin Sabrina eine Webentwicklerin am Ammersee, südwestlich von München, die richtig programmieren kann!</b></p>
<p>Egal wie kreativ, individuell oder ausgefallen deine Idee ist ich finde einen Weg, sie genau so umzusetzen. Falls es mal schwierig wird, stehe ich dir jederzeit unterstützend zur Seite, oder ich setze alles komplett ohne starre Templates um, ganz nach deinen Vorstellungen.</p>
<p>Gemeinsam erschaffen wir Webseiten, die dich und deinen Kunden begeistern werden!</p>
</div>
</div>
</div>
</section>
<section class="cooperation">
<div class="container">
<h2>Mehr Freiheit für dein Design</h2>
<ul>
<li><b>Pixelgenaue Umsetzung</b> <br>Ich setze dein Design so nah wie möglich am Original um und achte dabei auf jedes Detail.</li>
<li><b>Individuelle Lösungen & komplexe Funktionen</b> <br>Egal ob API-Anbindung, dynamische Inhalte oder interaktive Features ich finde eine Lösung für fast alles.</li>
<li><b>Performance & Ladegeschwindigkeit</b> <br>Schnelle & schlanke Webseiten, die auf jedem Gerät reibungslos laufen.</li>
<li><b>Tool-übergreifende Kompetenz</b> <br>Kein WordPress-Zwang ich nutze die beste Lösung für dein Projekt, unabhängig vom System.</li>
</ul>
<p><b>Du bleibst der kreative Kopf!</b><br>
Ich sorge dafür, dass dein Design genauso im Web erscheint, wie du es geplant hast mit Top-Performance & perfekter technischer Umsetzung.</p>
<button class="pinkBtn" @click.prevent="toggleContactBubble" role="button">Lass uns kennenlernen</button>
</div>
</section>
<section class="deviceCheck" v-if="false">
<div class="container">
<div class="row">
<div class="col-md-6">
<p class="supheadlinePink">Responsive Check</p>
<h2>Wie sieht deine Webseite auf den verschiedenen Bildschirmbreiten aus?</h2>
<h3>Probiers einfach aus!</h3>
<input type="text" placeholder="https://www.deinewebseite.de" v-model="weblink" @keyup.enter="updateIframe">
</div>
<div class="col-md-6">
<svg v-if="desk" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 450">
<defs>
<clipPath id="_clipPath_mz11Urxh3q4HMztuYrKSFIZi2059ENKc">
<rect width="500" height="450"/>
</clipPath>
</defs>
<g clip-path="url(#_clipPath_mz11Urxh3q4HMztuYrKSFIZi2059ENKc)" style="z-index: 10">
<linearGradient id="_lgradient_0" x1="0.49993063593634346" y1="1.0075692569857018" x2="0.5082382316706063" y2="0.6847598402209258" gradientTransform="matrix(175.952,0,0,65.207,161.506,313.536)" gradientUnits="userSpaceOnUse">
<stop offset="0.8699999999999999%" stop-opacity="1" style="stop-color:rgb(209,209,209)"/>
<stop offset="47.83%" stop-opacity="1" style="stop-color:rgb(232,232,232)"/>
<stop offset="100%" stop-opacity="1" style="stop-color:rgb(209,209,209)"/>
</linearGradient>
<path d=" M 305.374 354.758 L 305.374 313.536 L 193.592 313.536 L 193.592 353.951 L 161.506 378.743 L 249.483 377.869 L 337.458 378.743 L 305.374 354.758 Z " fill="url(#_lgradient_0)" vector-effect="non-scaling-stroke" stroke-width="1" stroke="rgb(217,215,215)" stroke-opacity="100" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/>
<linearGradient id="_lgradient_1" x1="0.0000010284238562002779" y1="0.499981826442526" x2="0.9999991252331814" y2="0.499981826442526" gradientTransform="matrix(443,0,0,43.47,28.5,275.242)" gradientUnits="userSpaceOnUse">
<stop offset="1.7399999999999998%" stop-opacity="1" style="stop-color:rgb(209,209,209)"/>
<stop offset="100%" stop-opacity="1" style="stop-color:rgb(235,235,235)"/>
</linearGradient>
<path d=" M 34.273 275.242 L 465.964 275.242 C 469.02 275.242 471.5 277.728 471.5 280.79 L 471.5 297.862 C 471.5 309.369 462.177 318.712 450.697 318.712 L 49.302 318.712 C 37.822 318.712 28.5 309.369 28.5 297.862 L 28.5 281.027 C 28.5 277.834 31.087 275.242 34.273 275.242 Z " fill="url(#_lgradient_1)" vector-effect="non-scaling-stroke" stroke-width="1" stroke="rgb(217,215,215)" stroke-opacity="100" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/>
<path d=" M 472.012 254.78 L 472.012 51.832 C 472.012 40.335 462.945 31 451.776 31 L 48.223 31 C 37.055 31 27.988 40.335 27.988 51.832 L 27.988 254.78 L 27.988 255.259 L 27.988 282.486 L 472.012 282.486 L 472.012 255.259 L 472.012 254.78 Z M 45.583 46.984 L 454.416 46.984 L 454.416 268.633 L 45.583 268.633 L 45.583 46.984 Z " fill-rule="evenodd" fill="rgb(55,55,55)"/>
<path d="M 73.451 74.227 L 133.299 74.227 L 133.299 134.075 L 73.451 134.075 L 73.451 74.227 Z" style="stroke:none;fill:#F0F0F0;stroke-miterlimit:10;"/>
<linearGradient id="_lgradient_2" x1="-0.0000019194307706443814" y1="0.5000543177381012" x2="0.9999980747113533" y2="0.5000543177381012" gradientTransform="matrix(306.503,0,0,9.583,96.179,370.194)" gradientUnits="userSpaceOnUse">
<stop offset="0.8699999999999999%" stop-opacity="1" style="stop-color:rgb(162,162,162)"/>
<stop offset="97.83%" stop-opacity="1" style="stop-color:rgb(232,232,232)"/>
</linearGradient>
<path d=" M 98.371 370.463 C 102.752 369.979 396.421 370.256 400.595 370.463 C 403.378 370.6 403.378 373.706 400.595 379.777 L 98.371 379.777 C 95.449 373.89 95.449 370.785 98.371 370.463 Z " fill="url(#_lgradient_2)" vector-effect="non-scaling-stroke" stroke-width="1" stroke="rgb(185,185,185)" stroke-opacity="100" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/>
<rect x="45.583" y="46.984" width="408.832" height="222.047" transform="matrix(1,0,0,1,0,0)" fill="rgb(0,0,0)"/>
<path d=" M 243.844 302.371 C 242.175 299.831 245.066 297.867 247.098 297.222 C 251.729 295.751 255.722 297.863 254.907 301.23 C 253.327 307.756 245.055 304.215 243.844 302.371 Z " fill="rgb(55,55,55)"/>
<rect id="deskScreenBlank" x="46" y="47.5" width="409.5" height="221.5" transform="matrix(1,0,0,1,0,0)" fill="rgb(0,0,0)"/>
<g>
<foreignObject v-if="isValidUrl(weblink)" id="deskScreen" x="46" y="47.5" width="409.5" height="221.5">
<iframe ref="iframeRef" :src="weblink" frameborder="0"></iframe>
</foreignObject>
</g>
</g>
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<defs>
<clipPath id="_clipPath_J5sSadUalMgzHmiGMPTuwCKeG9KghIf7">
<rect width="500" height="500"/>
</clipPath>
</defs>
<g clip-path="url(#_clipPath_J5sSadUalMgzHmiGMPTuwCKeG9KghIf7)">
<path d=" M 152.048 12.143 L 352.475 12.143 C 368.401 12.143 381.331 25.073 381.331 40.999 L 381.331 451.861 C 381.331 467.787 368.401 480.716 352.475 480.716 L 152.048 480.716 C 136.122 480.716 123.192 467.787 123.192 451.861 L 123.192 40.999 C 123.192 25.073 136.122 12.143 152.048 12.143 Z " fill="rgb(55,55,55)"/>
<path d="M 155.466 25.45 L 347.174 25.45 C 355.373 25.45 362.03 32.107 362.03 40.306 L 362.03 438.864 C 362.03 447.063 355.373 453.72 347.174 453.72 L 155.466 453.72 C 147.267 453.72 140.61 447.063 140.61 438.864 L 140.61 40.306 C 140.61 32.107 147.267 25.45 155.466 25.45 Z" style="stroke:none;fill:#000000;stroke-miterlimit:10;"/>
<mask id="_mask_kRhtIUM0vsIJwCru8KMEeoX7mwdCHdLR">
<path d=" M 241.815 465.584 C 241.815 461.174 245.395 457.594 249.804 457.594 C 254.214 457.594 257.794 461.174 257.794 465.584 C 257.794 469.993 254.214 473.573 249.804 473.573 C 245.395 473.573 241.815 469.993 241.815 465.584 Z " fill="white" stroke="none"/>
</mask>
<path d=" M 241.815 465.584 C 241.815 461.174 245.395 457.594 249.804 457.594 C 254.214 457.594 257.794 461.174 257.794 465.584 C 257.794 469.993 254.214 473.573 249.804 473.573 C 245.395 473.573 241.815 469.993 241.815 465.584 Z " fill="rgb(49,49,49)"/>
<path d=" M 241.815 465.584 C 241.815 461.174 245.395 457.594 249.804 457.594 C 254.214 457.594 257.794 461.174 257.794 465.584 C 257.794 469.993 254.214 473.573 249.804 473.573 C 245.395 473.573 241.815 469.993 241.815 465.584 Z " fill="rgb(49,49,49)" mask="url(#_mask_kRhtIUM0vsIJwCru8KMEeoX7mwdCHdLR)" vector-effect="non-scaling-stroke" stroke-width="4" stroke="rgb(119,119,119)" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/>
<path d=" M 210.188 13.882 L 294.187 13.882 C 298.869 13.882 302.669 17.683 302.669 22.364 L 302.669 22.739 C 302.669 27.42 298.869 31.221 294.187 31.221 L 210.188 31.221 C 205.506 31.221 201.706 27.42 201.706 22.739 L 201.706 22.364 C 201.706 17.683 205.506 13.882 210.188 13.882 Z " fill="rgb(55,55,55)"/>
<clipPath id="_clipPath_yG0PUG5xuGwoBQdYIaTfyJw0eBJTI7af">
<path d=" M 210.188 13.882 L 294.187 13.882 C 298.869 13.882 302.669 17.683 302.669 22.364 L 302.669 22.739 C 302.669 27.42 298.869 31.221 294.187 31.221 L 210.188 31.221 C 205.506 31.221 201.706 27.42 201.706 22.739 L 201.706 22.364 C 201.706 17.683 205.506 13.882 210.188 13.882 Z " fill="rgb(55,55,55)"/>
</clipPath>
<g clip-path="url(#_clipPath_yG0PUG5xuGwoBQdYIaTfyJw0eBJTI7af)">
<mask id="_mask_toYsxxmj7yA1tlh5Xolo04GF3EKtC9b5">
<path d=" M 248.906 23.145 C 248.906 21.069 250.591 19.384 252.667 19.384 C 254.743 19.384 256.429 21.069 256.429 23.145 C 256.429 25.221 254.743 26.907 252.667 26.907 C 250.591 26.907 248.906 25.221 248.906 23.145 Z " fill="white" stroke="none"/>
</mask>
<path d=" M 248.906 23.145 C 248.906 21.069 250.591 19.384 252.667 19.384 C 254.743 19.384 256.429 21.069 256.429 23.145 C 256.429 25.221 254.743 26.907 252.667 26.907 C 250.591 26.907 248.906 25.221 248.906 23.145 Z " fill="rgb(157,157,157)"/>
<path d=" M 248.906 23.145 C 248.906 21.069 250.591 19.384 252.667 19.384 C 254.743 19.384 256.429 21.069 256.429 23.145 C 256.429 25.221 254.743 26.907 252.667 26.907 C 250.591 26.907 248.906 25.221 248.906 23.145 Z " fill="rgb(157,157,157)" mask="url(#_mask_toYsxxmj7yA1tlh5Xolo04GF3EKtC9b5)" vector-effect="non-scaling-stroke" stroke-width="2" stroke="rgb(181,181,181)" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/>
</g>
</g>
</svg>
</div>
</div>
</div>
</section>
<Recommendations />
<section class="contrastCalcLink">
<div class="container">
<img src="https://strapi.digimedialoop.de/uploads/wcag_kontrastrechner_77abf9d9be.png" alt="kontrast check" class="imgRight">
<p class="supheadlinePink">Barrierefreies Webdesign</p>
<h2>Hat Dein Design den richtigen Kontrast?</h2>
<p>Mit dem praktischen Kontrastchecker kannst Du herausfinden, ob die Schrift und der Hintergrund in Deinem Design den WCAG_Richtlinien entsprechen. So stellst Du sicher, dass Dein Design für alle gut lesbar ist.</p>
<button class="mintBtn" @click.prevent="navigateTo('/toolbox/kontrastchecker')"
role="button"
aria-label="Zum Kontrastchecker">Jetzt kostenlos Konstrast prüfen</button>
</div>
</section>
<FAQArea
pageLink="/webentwicklung-fuer-designer-und-mediengestalter"
headline="Wichtige Antworten zur Zusammenarbeit im Überblick für Dich"
button="Dann lass uns quatschen!"
/>
</div>
</template>
<script setup>
definePageMeta({
name: 'designer'
})
import { ref, onMounted, onUnmounted, nextTick } from "vue";
import { useMainStore } from '@/stores/main';
// Lade diese Komponente synchron wegen SEO
const mainStore = useMainStore();
const toggleContactBubble = () => mainStore.toggleContactBubble();
const weblink = ref("https://digimedialoop.de");
const iframeRef = ref(null);
const observer = ref(null);
const desk = ref(true)
// Berechnet die exakte Größe des `foreignObject` basierend auf `#deskScreenBlank`
const updateIframeSize = () => {
nextTick(() => {
const blankRect = document.getElementById("deskScreenBlank");
const foreignObject = document.getElementById("deskScreen");
if (blankRect && foreignObject && iframeRef.value) {
// Exakte Werte auslesen und Float-Werte setzen (verhindert Subpixel-Rundungen)
const currentWidth = parseFloat(blankRect.getAttribute("width"));
const currentHeight = parseFloat(blankRect.getAttribute("height"));
// Setze `foreignObject` exakt auf `#deskScreenBlank`
foreignObject.setAttribute("width", currentWidth.toFixed(2));
foreignObject.setAttribute("height", currentHeight.toFixed(2));
// Berechne den Skalierungsfaktor für 1400px Breite
const zoomFactor = currentWidth / 1400;
// Setze das `iframe` korrekt
iframeRef.value.style.width = "1400px"; // Fixierte Breite
iframeRef.value.style.height = "755px"; // Höhe für Scrollbarkeit
iframeRef.value.style.zoom = zoomFactor; // Zoom-Faktor
iframeRef.value.style.overflowY = "scroll"; // Scrollen aktivieren
}
});
};
// Startet `ResizeObserver`, um live Änderungen zu tracken
const startObserver = () => {
const blankRect = document.getElementById("deskScreenBlank");
if (blankRect) {
observer.value = new ResizeObserver(updateIframeSize);
observer.value.observe(blankRect);
}
};
// URL aktualisieren & Größe anpassen
const updateIframe = () => {
if (!weblink.value.trim()) return;
weblink.value = ensureValidUrl(weblink.value);
if (!isValidUrl(weblink.value)) {
alert("Bitte eine gültige URL eingeben.");
return;
}
updateIframeSize();
};
// Automatisch "https://" hinzufügen, falls nötig
const ensureValidUrl = (url) => {
if (!url.match(/^https?:\/\//)) {
return "https://" + url;
}
return url;
};
// Prüft, ob die URL gültig ist
const isValidUrl = (url) => {
try {
new URL(url);
return true;
} catch (_) {
return false;
}
};
// Initialisiere `ResizeObserver` beim Mounten
onMounted(() => {
startObserver();
updateIframeSize();
});
// Entferne Observer, wenn die Komponente zerstört wird
onUnmounted(() => {
if (observer.value) observer.value.disconnect();
});
</script>
<style lang="sass">
.landing
section
margin: 5vh auto
h1
margin-bottom: 1rem
h3
line-height: 150%
font-size: 1.4rem
.heroBox
position: relative
overflow-x: hidden
overflow-y: hidden
/*&::after
content: ''
position: absolute
top: 2vh
right: -50vw
width: 80vw
height: 90%
min-height: 500px
max-height: 800px
background-image: url('https://strapi.digimedialoop.de/uploads/Screen_Shot_Tool_20250225214638_2209c9c92e.png')
background-repeat: no-repeat
background-position: center right
background-size: cover
border-radius: 42% 49% 52% 48% / 53% 38% 62% 47%
animation: bubble-wobble 25s infinite ease alternate, gradient-animation 70s infinite alternate ease-in-out
box-shadow: $innerShadow
opacity: 1
@media(max-width: $breakPointXL)
right: -60vw
@media(max-width: $breakPointLG)
right: -60vw
min-height: 300px
max-height: 500px
@media(max-width: $breakPointLG)
min-height: 200px
max-height: 400px
right: -65vw
@media(max-width: $breakPointLG)
min-height: 200px
max-height: 400px
right: -70vw */
h2
margin-bottom: 1rem
font-size: 1.4rem
h3
font-size: 1.1rem
font-family: 'Mainfont-Bold'
margin-bottom: 1rem
.contentBox
width: 95%
// width: 55vw
//@media(max-width: $breakPointMD)
//width: 68vw
.speechBox
display: flex
align-items: center
max-width: 100%
margin: 0 auto
padding: 1rem 0
gap: 4vw
.profileImage
width: 30vw
margin-left: -3vw
max-width: 260px
border-radius: $loopShape
flex-shrink: 0
background-color: rgba(white, .7)
//animation: bubble-wobble 15s infinite ease alternate, gradient-animation 10s infinite alternate ease-in-out
.bubble
position: relative
//background-image: linear-gradient(to right, lighten($beige, 5%), white)
border: 1px solid darken($beige, 5%)
border-radius: 1rem
padding: 1.5rem
width: 100%
max-width: 450px
line-height: 1.4
span
color: darken($pink, 5%)
font-size: 1.2rem
text-transform: uppercase
padding: 1rem 0
p
font-size: .9rem
&:before
content: ''
position: absolute
top: 40px
left: -12px
width: 20px
height: 20px
background: white //lighten($beige, 5%)
border-left: 1px solid darken($beige, 5%)
border-top: 1px solid darken($beige, 5%)
transform: rotate(-45deg)
@media(max-width: $breakPointMD)
.cooperation
margin: 0 0 5rem 0
ul
list-style: none
padding: 0 0 1rem 1rem
li
position: relative
padding-left: 2rem
font-size: 1rem
margin-bottom: 1rem
&::before
content: "✔"
color: $primaryColor
font-weight: bold
position: absolute
font-size: 1.4rem
left: 0
.contrastCalcLink
margin-bottom: 10vh
img
width: 100%
.deviceCheck
h2
margin-bottom: .5rem
input
width: 100%
padding: .5rem 1rem
border: 1px solid lightgrey
border-radius: 5px
font-size: 1.2rem
color: $darkgrey
#deskScreen
width: 100%
height: 100%
position: relative
overflow: hidden
iframe
width: 1400px // Fixierte virtuelle Breite
height: auto // Automatische Höhe für Scrollen
border: none
display: block
overflow-y: auto // Scrollen aktivieren
</style>

View File

@ -1,7 +1,5 @@
<template> <template>
<div class="container-10"> <PageContent />
<h1>{{ $t('imprint') }}</h1>
</div>
</template> </template>
<script setup> <script setup>

View File

@ -1,9 +1,10 @@
<template> <template>
<div class="homePage"> <div class="homePage">
<section class="heroBox_service" aria-labelledby="hero-heading">
<section class="heroBox" :aria-label="$t('pages.services.hero.ariaLabel')">
<NuxtImg <NuxtImg
provider="strapi" provider="strapi"
src="/uploads/large_DML_Home_Hero_4f27bc7f8e.webp" src="/uploads/bg_technologie_d24f950ccf.webp"
class="hero-bg" class="hero-bg"
sizes="sm:100vw md:100vw lg:100vw" sizes="sm:100vw md:100vw lg:100vw"
alt="" alt=""
@ -14,9 +15,9 @@
fetchpriority="high" fetchpriority="high"
/> />
<div class="container-10"> <div class="container-10">
<h1 id="hero-heading">{{ $t('pages.home.heroBox.h1') }}</h1> <h1>{{ $t('pages.services.hero.headline1') }}</h1>
<h2>{{ $t('pages.home.heroBox.h2') }}</h2> <h2>{{ $t('pages.services.hero.headline2') }}</h2>
<h3>{{ $t('pages.home.heroBox.h3') }}</h3> <h3>{{ $t('pages.services.hero.headline3') }}</h3>
</div> </div>
<!-- Nach dem Container: Spiegelwelle unten --> <!-- 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"> <svg class="sectionWave wave-bottom" style="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20" aria-hidden="true">
@ -25,84 +26,38 @@
</section> </section>
<section aria-labelledby="solution-title"> <section class="explainBox" :aria-label="$t('pages.services.explain.ariaLabel')">
<div class="container-10 webStrategy">
<h2 id="solution-title">{{ $t('pages.home.solution.title') }}</h2>
<h3>{{ $t('pages.home.solution.teaser') }}</h3>
<p>{{ $t('pages.home.solution.text') }}</p>
<button
class="mintBtn"
role="button"
aria-describedby="solution-title"
aria-label="headless CMS Info" @click="navigateToArticle">
{{ $t('pages.home.solution.buttonText') }}
</button>
</div>
</section>
<section class="targetGroup" aria-labelledby="invitation-title">
<svg class="sectionWave wave-top" 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>
<div class="container-10">
<div class="row">
<div class="col-md-4"/>
<div class="col-md-8 pt-5 pb-5">
<h2 id="invitation-title">{{ $t('pages.home.invitation.title') }}</h2>
<h3>{{ $t('pages.home.invitation.teaser') }}</h3>
<button
class="pinkBtn" role="button"
aria-describedby="invitation-title"
aria-label="Kontaktformular öffnen"
@click.prevent="toggleContactBubble">{{ $t('pages.home.invitation.button') }}</button>
</div>
</div>
</div>
<svg class="sectionWave wave-bottom" style="transform: scale(-1,-1)" 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="canDo" aria-labelledby="canDo-section-title">
<div class="container">
<h2 id="canDo-section-title" class="text-center">
{{ $t('pages.home.canDo.title') }}
</h2>
<div class="canDoGrid">
<div
v-for="(item, index) in canDoItems"
:key="index"
class="canDoBox"
>
<div
class="canDoItem"
:role="index === 2 ? 'group' : null"
:aria-labelledby="index === 2 ? 'cando-title' : null"
>
<NuxtImg <NuxtImg
src="/uploads/Human_Maschine_BG_5c91e9100f.webp"
provider="strapi" provider="strapi"
:src="item.img"
format="webp" format="webp"
:modifiers="{ quality: 80 }" sizes="100vw"
sizes="xs:280px sm:300px md:350px" class="background-image"
class="imageBox" alt=""
loading="lazy" aria-hidden="true"
:alt="item.alt"
/> />
<h3 :id="index === 2 ? 'cando-title' : null"> <div class="container-15 content">
{{ $t(item.title) }} <h2>{{ $t('pages.services.explain.headline1') }}</h2>
</h3> <h3>{{ $t('pages.services.explain.headline2') }}</h3>
<p>{{ $t(item.text) }}</p>
</div> <p>{{ $t('pages.services.explain.paragraph') }}</p>
</div> <ul class="check">
</div> <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> </div>
</section> </section>
<section class="compBox"> <section class="compBox">
<svg class="sectionWave wave-top" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20"> <svg class="sectionWave wave-top" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"/> <path 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"/>
@ -152,16 +107,10 @@ import { defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
const router = useRouter(); const router = useRouter();
const runtimeConfig = useRuntimeConfig();
const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl)
const mainStore = useMainStore(); const mainStore = useMainStore();
const { customers } = storeToRefs(mainStore); const { customers } = storeToRefs(mainStore);
const toggleContactBubble = () => mainStore.toggleContactBubble(); const toggleContactBubble = () => mainStore.toggleContactBubble();
const navigateToArticle = () => {
router.push('/wissenswertes/artikel/design-und-inhalt-sauber-getrennt-warum-headless-webdesign-die-beste-wahl-fuer-moderne-unternehmen-ist');
};
const screenWidth = computed(() => mainStore.screenWidth); const screenWidth = computed(() => mainStore.screenWidth);
const waveHeight = computed(() => (screenWidth.value / 25).toFixed(0)); const waveHeight = computed(() => (screenWidth.value / 25).toFixed(0));
@ -176,32 +125,7 @@ const logoItems = computed(() => {
})) }))
}) })
const canDoItems = [
{
img: '/uploads/website_Erfolg_Marketing_3c36a43ba5.png',
alt: 'Website Erfolg Marketing',
title: 'pages.home.canDo.item1.title',
text: 'pages.home.canDo.item1.text',
},
{
img: '/uploads/Kundenbindung_45d45ef3fc.png',
alt: 'Kundenbindung Strategie',
title: 'pages.home.canDo.item2.title',
text: 'pages.home.canDo.item2.text',
},
{
img: '/uploads/Screen_Shot_Tool_20250228133408_beb2a11980.png',
alt: 'Screen Tool Beispiel 1',
title: 'pages.home.canDo.item3.title',
text: 'pages.home.canDo.item3.text',
},
{
img: '/uploads/Screen_Shot_Tool_20250228133812_0a20d4320e.png',
alt: 'Screen Tool Beispiel 2',
title: 'pages.home.canDo.item4.title',
text: 'pages.home.canDo.item4.text',
},
];
</script> </script>
@ -209,7 +133,7 @@ const canDoItems = [
<style lang="sass"> <style lang="sass">
.homePage .homePage
.heroBox_service .heroBox
position: relative position: relative
min-height: 35rem min-height: 35rem
height: 70vh height: 70vh
@ -218,108 +142,87 @@ const canDoItems = [
justify-content: center justify-content: center
overflow: hidden overflow: hidden
&::after
position: absolute
content: ""
z-index: 0
top: 0
left: 0
width: 100%
height: 100%
background-image: linear-gradient(
to bottom right,
rgba(255,255,255,0.4) 0%,
rgba(255,255,255,0.3) 2%,
rgba(0, 0, 0, 0.7) 60%,
rgba(255, 255, 255, 0) 100%,
transparent 100%
)
.hero-bg .hero-bg
position: absolute position: absolute
inset: 0 inset: 0
width: 100% width: 100%
height: 100% height: 100%
object-fit: cover object-fit: cover
object-position: center bottom object-position: center top
z-index: 0 z-index: -1
.container-10, h1, h2, h3 .container-10, h1, h2, h3
position: relative position: relative
z-index: 1 z-index: 1
h1, h2, h3
h1 color: white
margin-top: 3rem
z-index: 2 z-index: 2
color: mix(black, $pink, 2%)
font-size: clamp(1rem, .8rem + 1vw, 1.2rem)
line-height: 1.5 line-height: 1.5
margin-bottom: 0
max-width: 70% max-width: 70%
@media (max-width: $breakPointMD) @media (max-width: $breakPointMD)
max-width: 100% max-width: 100%
h2 h1
z-index: 2 margin-top: 0
font-size: clamp(1.4rem, .8rem + 2vw, 2.4rem) font-size: clamp(2rem, 1.4rem + 3vw, 2.8rem)
line-height: 1.5 margin-bottom: 0
margin-top: 1rem font-family: 'Comfortaa'
max-width: 55% h2
font-size: clamp(1.2rem, .7rem + 2vw, 2rem)
margin: .8rem 0 .8rem 0
font-family: 'Comfortaa' font-family: 'Comfortaa'
@media (max-width: $breakPointMD)
max-width: 90%
h3 h3
max-width: 55% font-size: 1.2rem
line-height: 1.5
font-size: clamp(1rem, .8rem + 1vw, 1.6rem) .explainBox
@media (max-width: $breakPointMD) position: relative
max-width: 90% overflow: hidden
&::after min-height: 400px
content: '' margin: 5vh 0
padding: 8vh 0
.background-image
position: absolute position: absolute
top: 0 top: 0
left: 0 left: 0
width: 50% width: 100%
height: 100% height: auto
background-image: linear-gradient(to right, rgba(white, .3), transparent) object-fit: contain
z-index: 1 object-position: center center
.container z-index: 0
z-index: 2 pointer-events: none
.content
.webStrategy
padding: 4rem 0 3.5rem 0
h2
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
line-height: 150%
font-family: 'Comfortaa'
margin-left: 30%
h3
font-size: clamp(1.1rem, .75rem + 1vw, 1.25rem)
line-height: 150%
margin-left: 30%
p
margin-left: 30%
button
margin-left: 30%
&::after
content: ''
position: absolute
top: 5%
left: -36vw
width: 60vw
height: 90%
min-height: 550px
max-height: 800px
background-image: url('https://strapi.digimedialoop.de/uploads/Net_f1020a2216.png')
background-repeat: no-repeat
background-position: center right
background-size: cover
border-radius: 42% 49% 52% 48% / 53% 38% 62% 47%
animation: bubble-wobble 25s infinite ease alternate, gradient-animation 70s infinite alternate ease-in-out
box-shadow: $innerShadow
@media(max-width: $breakPointMD)
right: -50vw
.targetGroup
background-image: url('https://strapi.digimedialoop.de/uploads/smartphone_Contacts_40ae56a178.jpg')
background-repeat: no-repeat
background-size: cover
background-position: center top
min-height: 70vh
display: flex
align-items: center
justify-content: center
position: relative position: relative
padding: 3rem 0 z-index: 1
padding-left: 10%
h2 h2
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem) font-size: clamp(1.2rem, .7rem + 2vw, 2rem)
font-family: 'Comfortaa'
h3 h3
font-size: clamp(1.1rem, .8rem + 1vw, 1.2rem) font-family: 'Mainfont-Bold'
line-height: 150% font-size: 1.2rem
.firstTeaser .firstTeaser
h2 h2
@ -349,71 +252,6 @@ const canDoItems = [
padding-right: 1rem padding-right: 1rem
.canDo
margin: 12vh 0
h2
margin-bottom: 3.5rem
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
.canDoGrid
display: grid
gap: 2rem
justify-content: center
align-items: stretch
grid-template-columns: 1fr // Default: 1 Spalte
@media (min-width: $breakPointMD)
grid-template-columns: repeat(2, 1fr)
@media (min-width: $breakPointXL)
grid-template-columns: repeat(4, 1fr)
.canDoBox
width: 100%
display: flex
flex-direction: column
align-items: center
justify-content: flex-start
background-image: linear-gradient(to bottom right, transparent , white)
box-shadow: 3px 3px 8px 1px $lightgrey
border-bottom-right-radius: 1rem
padding: 1rem 1.5rem
border-right: 1px solid lighten($beige, 0%)
border-bottom: 1px solid lighten($beige, 0%)
height: 100%
.canDoItem
width: 100%
.imageBox
margin: 0rem auto 1rem auto
width: 100%
max-width: 180px
aspect-ratio: 5 / 4
object-fit: cover
border-radius: 1rem
display: block
&:nth-child(1)
border-radius: $loopShape1
&:nth-child(2)
border-radius: $loopShape2
&:nth-child(3)
border-radius: $loopShape3
&:nth-child(4)
border-radius: $loopShape4
h3
font-size: 1.2rem
line-height: 1.5rem
text-align: center
font-family: 'Mainfont-Bold'
color: darken($pink, 10%)
text-transform: uppercase
margin-bottom: .5rem
p
font-size: .9rem
text-align: left !important

View File

@ -1,15 +1,170 @@
<template> <template>
<div class="container-10"> <div class="knowledgeBox topSpace">
<h1>{{ $t('magazin') }}</h1> <section class="teaserBox">
<div class="container">
<h1>{{ $t('pages.magazin.title') }}</h1>
<p>{{ $t('pages.magazin.teaser1') }}</p>
<p>{{ $t('pages.magazin.teaser2') }}</p>
</div>
</section>
<section class="articleBox container">
<div class="grid">
<div
v-for="article in articles"
:key="article.id"
class="article"
>
<NuxtLink :to="getArticleLink(article.slug)" class="article-link">
<div class="image-wrapper">
<NuxtImg
v-if="article.image?.url"
:src="article.image.url"
provider="strapi"
:alt="article.image.alternativeText"
format="webp"
class="article-image"
/>
<div class="overlay">
<h2>{{ article.header }}</h2>
<button class="mintBtn">{{ $t('pages.magazin.readmore') }}</button>
</div>
</div>
</NuxtLink>
</div>
</div>
</section>
</div> </div>
</template> </template>
<script setup>
definePageMeta({ <script setup lang="ts">
layout: 'default' import { computed, watch } from 'vue'
import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'
const mainStore = useMainStore()
const { articles, cmsUrl } = storeToRefs(mainStore)
const { getArticleLink } = useI18nPages()
const truncateText = (text: string, length = 200) =>
text?.length > length ? text.substring(0, length) + '…' : text
const currentDomain = typeof window !== 'undefined'
? window.location.origin
: 'https://www.digimedialoop.de'
// SEO: JSON-LD für Artikelübersicht
watch(articles, (newVal) => {
if (newVal?.length) {
useHead({
script: [
{
type: 'application/ld+json',
children: JSON.stringify({
"@context": "https://schema.org",
"@type": "ItemList",
itemListElement: newVal.map((article, index) => ({
"@type": "ListItem",
position: index + 1,
url: `${currentDomain}/wissenswertes/artikel/${article.slug}`,
name: article.header,
}))
}) })
}
]
})
}
}, { immediate: true })
</script> </script>
<style lang="sass">
<style lang="sass">
.articleBox
display: flex
justify-content: center
width: 100%
.grid
display: grid
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr))
gap: 1rem
justify-content: start
width: 100%
max-width: 100%
margin: 0 auto
.article
width: 100%
max-width: 450px
border: 1px solid $beige
background: linear-gradient(to bottom right, white, $lightgrey)
border-radius: 1rem
transition: .8s
position: relative
display: flex
flex-direction: column
align-items: flex-start
overflow: hidden
.article-link
position: relative
display: block
color: white
text-decoration: none
.image-wrapper
position: relative
width: 100%
height: 220px
.article-image
width: 100%
height: 450px
object-fit: cover
border-top-left-radius: 1rem
border-top-right-radius: 1rem
.overlay
position: absolute
top: 0
left: 0
width: 100%
height: 100%
background-color: rgba(0, 0, 0, 0.7)
display: flex
flex-direction: column
align-items: center
justify-content: center
padding: 1rem
border-top-left-radius: 1rem
border-top-right-radius: 1rem
text-align: center
transition: .5s
&:hover
background-color: rgba(255, 255, 255, 0.8)
h2
color: black
h2
color: white
font-size: 1.1rem
line-height: 130%
font-family: 'Mainfont-Bold'
margin-bottom: 0.5rem
.mintBtn
background-color: $primaryColor
color: white
font-size: 0.9rem
font-family: 'Mainfont-Bold'
border: none
padding: 0.4rem 1rem
border-radius: 0.3rem
cursor: pointer
</style> </style>

View File

@ -1,7 +1,5 @@
<template> <template>
<div class="container-10"> <PageContent />
<h1>{{ $t('privacy') }}</h1>
</div>
</template> </template>
<script setup> <script setup>

View File

@ -1,9 +1,9 @@
<template> <template>
<div> <div>
<section class="heroBox" :aria-label="$t('pages.services.hero.ariaLabel')"> <section class="heroBox_service" aria-labelledby="hero-heading">
<NuxtImg <NuxtImg
provider="strapi" provider="strapi"
src="/uploads/BG_technology_b6b0083811.png" src="/uploads/large_DML_Home_Hero_4f27bc7f8e.webp"
class="hero-bg" class="hero-bg"
sizes="sm:100vw md:100vw lg:100vw" sizes="sm:100vw md:100vw lg:100vw"
alt="" alt=""
@ -14,9 +14,9 @@
fetchpriority="high" fetchpriority="high"
/> />
<div class="container-10"> <div class="container-10">
<h1>{{ $t('pages.services.hero.headline1') }}</h1> <h1 id="hero-heading">{{ $t('pages.home.heroBox.h1') }}</h1>
<h2>{{ $t('pages.services.hero.headline2') }}</h2> <h2>{{ $t('pages.home.heroBox.h2') }}</h2>
<h3>{{ $t('pages.services.hero.headline3') }}</h3> <h3>{{ $t('pages.home.heroBox.h3') }}</h3>
</div> </div>
<!-- Nach dem Container: Spiegelwelle unten --> <!-- 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"> <svg class="sectionWave wave-bottom" style="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20" aria-hidden="true">
@ -25,28 +25,78 @@
</section> </section>
<section class="explainBox" :aria-label="$t('pages.services.explain.ariaLabel')"> <section aria-labelledby="solution-title">
<NuxtImg <div class="container-10 webStrategy">
src="/uploads/Human_Maschine_BG_5c91e9100f.webp" <h2 id="solution-title">{{ $t('pages.home.solution.title') }}</h2>
provider="strapi" <h3>{{ $t('pages.home.solution.teaser') }}</h3>
format="webp" <p>{{ $t('pages.home.solution.text') }}</p>
sizes="100vw" <button
class="background-image" class="mintBtn"
alt="" role="button"
aria-hidden="true" aria-describedby="solution-title"
/> aria-label="headless CMS Info" @click="navigateToArticle">
<div class="container-15 content"> {{ $t('pages.home.solution.buttonText') }}
<h2>{{ $t('pages.services.explain.headline1') }}</h2> </button>
<h3>{{ $t('pages.services.explain.headline2') }}</h3> </div>
</section>
<p>{{ $t('pages.services.explain.paragraph') }}</p> <section class="targetGroup" aria-labelledby="invitation-title">
<ul class="check"> <svg class="sectionWave wave-top" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20" aria-hidden="true">
<li>{{ t('pages.services.explain.bullet1') }}</li> <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"/>
<li>{{ t('pages.services.explain.bullet2') }}</li> </svg>
<li>{{ t('pages.services.explain.bullet3') }}</li> <div class="container-10">
<li>{{ t('pages.services.explain.bullet4') }}</li> <div class="row">
<li>{{ t('pages.services.explain.bullet5') }}</li> <div class="col-md-4"/>
</ul> <div class="col-md-8 pt-5 pb-5">
<h2 id="invitation-title">{{ $t('pages.home.invitation.title') }}</h2>
<h3>{{ $t('pages.home.invitation.teaser') }}</h3>
<button
class="pinkBtn" role="button"
aria-describedby="invitation-title"
aria-label="Kontaktformular öffnen"
@click.prevent="toggleContactBubble">{{ $t('pages.home.invitation.button') }}</button>
</div>
</div>
</div>
<svg class="sectionWave wave-bottom" style="transform: scale(-1,-1)" 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="canDo" aria-labelledby="canDo-section-title">
<div class="container">
<h2 id="canDo-section-title" class="text-center">
{{ $t('pages.home.canDo.title') }}
</h2>
<div class="canDoGrid">
<div
v-for="(item, index) in canDoItems"
:key="index"
class="canDoBox"
>
<div
class="canDoItem"
:role="index === 2 ? 'group' : null"
:aria-labelledby="index === 2 ? 'cando-title' : null"
>
<NuxtImg
provider="strapi"
:src="item.img"
format="webp"
:modifiers="{ quality: 80 }"
sizes="xs:280px sm:300px md:350px"
class="imageBox"
loading="lazy"
:alt="item.alt"
/>
<h3 :id="index === 2 ? 'cando-title' : null">
{{ $t(item.title) }}
</h3>
<p>{{ $t(item.text) }}</p>
</div>
</div>
</div>
</div> </div>
</section> </section>
@ -82,6 +132,10 @@ const { t } = useI18n();
const mainStore = useMainStore(); const mainStore = useMainStore();
const { projects, companyinfo } = storeToRefs(mainStore) const { projects, companyinfo } = storeToRefs(mainStore)
const navigateToArticle = () => {
router.push('/wissenswertes/artikel/design-und-inhalt-sauber-getrennt-warum-headless-webdesign-die-beste-wahl-fuer-moderne-unternehmen-ist');
};
const projectItems = computed(() => { const projectItems = computed(() => {
return projects.value return projects.value
.filter(project => project.customer && project.projectImages.length > 0) .filter(project => project.customer && project.projectImages.length > 0)
@ -102,6 +156,33 @@ const logoUrl = computed(() => {
: origin + '/logo.svg'; : origin + '/logo.svg';
}) })
const canDoItems = [
{
img: '/uploads/website_Erfolg_Marketing_3c36a43ba5.png',
alt: 'Website Erfolg Marketing',
title: 'pages.home.canDo.item1.title',
text: 'pages.home.canDo.item1.text',
},
{
img: '/uploads/Kundenbindung_45d45ef3fc.png',
alt: 'Kundenbindung Strategie',
title: 'pages.home.canDo.item2.title',
text: 'pages.home.canDo.item2.text',
},
{
img: '/uploads/Screen_Shot_Tool_20250228133408_beb2a11980.png',
alt: 'Screen Tool Beispiel 1',
title: 'pages.home.canDo.item3.title',
text: 'pages.home.canDo.item3.text',
},
{
img: '/uploads/Screen_Shot_Tool_20250228133812_0a20d4320e.png',
alt: 'Screen Tool Beispiel 2',
title: 'pages.home.canDo.item4.title',
text: 'pages.home.canDo.item4.text',
},
];
// JSON_LD für Services // JSON_LD für Services
const jsonLdServices = computed(() => { const jsonLdServices = computed(() => {
@ -172,8 +253,7 @@ watchEffect(() => {
</script> </script>
<style lang="sass"> <style lang="sass">
.heroBox_service
.heroBox
position: relative position: relative
min-height: 35rem min-height: 35rem
height: 70vh height: 70vh
@ -188,56 +268,164 @@ watchEffect(() => {
width: 100% width: 100%
height: 100% height: 100%
object-fit: cover object-fit: cover
object-position: center center object-position: center bottom
z-index: 0 z-index: 0
.container-10, h1, h2, h3 .container-10, h1, h2, h3
position: relative position: relative
z-index: 1 z-index: 1
h1, h2, h3
color: white h1
margin-top: 3rem
z-index: 2 z-index: 2
color: mix(black, $pink, 2%)
font-size: clamp(1rem, .8rem + 1vw, 1.2rem)
line-height: 1.5 line-height: 1.5
margin-bottom: 0
max-width: 70% max-width: 70%
@media (max-width: $breakPointMD) @media (max-width: $breakPointMD)
max-width: 100% max-width: 100%
h1
margin-top: 0
font-size: clamp(2rem, 1.4rem + 3vw, 2.8rem)
margin-bottom: 0
font-family: 'Comfortaa'
h2 h2
font-size: clamp(1.2rem, .7rem + 2vw, 2rem) z-index: 2
margin: .8rem 0 .8rem 0 font-size: clamp(1.4rem, .8rem + 2vw, 2.4rem)
line-height: 1.5
margin-top: 1rem
max-width: 55%
font-family: 'Comfortaa' font-family: 'Comfortaa'
@media (max-width: $breakPointMD)
max-width: 90%
h3 h3
font-size: 1.2rem max-width: 55%
line-height: 1.5
.explainBox font-size: clamp(1rem, .8rem + 1vw, 1.6rem)
position: relative @media (max-width: $breakPointMD)
overflow: hidden max-width: 90%
min-height: 400px &::after
margin: 5vh 0 content: ''
padding: 8vh 0
.background-image
position: absolute position: absolute
top: 0 top: 0
left: 0 left: 0
width: 100% width: 50%
height: auto height: 100%
object-fit: contain background-image: linear-gradient(to right, rgba(white, .3), transparent)
object-position: center center
z-index: 0
pointer-events: none
.content
position: relative
z-index: 1 z-index: 1
padding-left: 10% .container
z-index: 2
.webStrategy
padding: 4rem 0 3.5rem 0
h2
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
line-height: 150%
font-family: 'Comfortaa'
margin-left: 30%
h3
font-size: clamp(1.1rem, .75rem + 1vw, 1.25rem)
line-height: 150%
margin-left: 30%
p
margin-left: 30%
button
margin-left: 30%
&::after
content: ''
position: absolute
top: 5%
left: -36vw
width: 60vw
height: 90%
min-height: 550px
max-height: 800px
background-image: url('https://strapi.digimedialoop.de/uploads/Net_f1020a2216.png')
background-repeat: no-repeat
background-position: center right
background-size: cover
border-radius: 42% 49% 52% 48% / 53% 38% 62% 47%
animation: bubble-wobble 25s infinite ease alternate, gradient-animation 70s infinite alternate ease-in-out
box-shadow: $innerShadow
@media(max-width: $breakPointMD)
right: -50vw
.canDo
margin: 12vh 0
h2
margin-bottom: 3.5rem
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
.canDoGrid
display: grid
gap: 2rem
justify-content: center
align-items: stretch
grid-template-columns: 1fr // Default: 1 Spalte
@media (min-width: $breakPointMD)
grid-template-columns: repeat(2, 1fr)
@media (min-width: $breakPointXL)
grid-template-columns: repeat(4, 1fr)
.canDoBox
width: 100%
display: flex
flex-direction: column
align-items: center
justify-content: flex-start
background-image: linear-gradient(to bottom right, transparent , white)
box-shadow: 3px 3px 8px 1px $lightgrey
border-bottom-right-radius: 1rem
padding: 1rem 1.5rem
border-right: 1px solid lighten($beige, 0%)
border-bottom: 1px solid lighten($beige, 0%)
height: 100%
.canDoItem
width: 100%
.imageBox
margin: 0rem auto 1rem auto
width: 100%
max-width: 180px
aspect-ratio: 5 / 4
object-fit: cover
border-radius: 1rem
display: block
&:nth-child(1)
border-radius: $loopShape1
&:nth-child(2)
border-radius: $loopShape2
&:nth-child(3)
border-radius: $loopShape3
&:nth-child(4)
border-radius: $loopShape4
h3 h3
font-family: 'Mainfont-Bold'
font-size: 1.2rem font-size: 1.2rem
line-height: 1.5rem
text-align: center
font-family: 'Mainfont-Bold'
color: darken($pink, 10%)
text-transform: uppercase
margin-bottom: .5rem
p
font-size: .9rem
text-align: left !important
.targetGroup
background-image: url('https://strapi.digimedialoop.de/uploads/smartphone_Contacts_40ae56a178.jpg')
background-repeat: no-repeat
background-size: cover
background-position: center top
min-height: 70vh
display: flex
align-items: center
justify-content: center
position: relative
padding: 3rem 0
h2
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
h3
font-size: clamp(1.1rem, .8rem + 1vw, 1.2rem)
line-height: 150%
</style> </style>

View File

@ -1,7 +1,5 @@
<template> <template>
<div class="container-10"> <PageContent />
<h1>{{ $t('termsOfService') }}</h1>
</div>
</template> </template>
<script setup> <script setup>

View File

@ -85,20 +85,20 @@
</div> </div>
</section> </section>
<section class="grafiker"> <!--<section class="grafiker">
<div class="container"> <div class="container">
<p class="supheadlineMint">{{ $t('pages.webagency.grafiker.supheadline') }}</p> <p class="supheadlineMint">{{ $t('pages.webagency.grafiker.supheadline') }}</p>
<h2>{{ $t('pages.webagency.grafiker.title') }}</h2> <h2>{{ $t('pages.webagency.grafiker.title') }}</h2>
<button <button
class="mintBtn" class="mintBtn"
@click.prevent="navigateTo('/webentwicklung-fuer-designer-und-mediengestalter')" @click.prevent="navigateTo(localePath({ name: 'designer' }))"
role="button" role="button"
aria-label="Zum Angebot für Kreative" aria-label="Zum Angebot für Kreative"
> >
{{ $t('pages.webagency.grafiker.button') }} {{ $t('pages.webagency.grafiker.button') }}
</button> </button>
</div> </div>
</section> </section>-->
</div> </div>
</template> </template>

View File

@ -0,0 +1,28 @@
export async function getSitemapRoutes() {
const CMS_BASE_URL = process.env.NUXT_PUBLIC_CMS_BASE_URL
const CMS_TOKEN = process.env.NUXT_PUBLIC_CMS_TOKEN
if (!CMS_BASE_URL || !CMS_TOKEN) {
throw new Error('CMS-Umgebungsvariablen fehlen')
}
const headers = {
Authorization: `Bearer ${CMS_TOKEN}`,
}
const [projectsRes, articlesRes] = await Promise.all([
$fetch(`${CMS_BASE_URL}/api/references?fields=link`, { headers }),
$fetch(`${CMS_BASE_URL}/api/newsarticels?fields=slug&locale=all`, { headers }),
])
const projectRoutes = projectsRes.data
.filter((p: any) => p.attributes.link)
.map((p: any) => `/projekt/${p.attributes.link}`)
const articleRoutes = articlesRes.data
.filter((a: any) => a.attributes.slug)
.map((a: any) => `/artikel/${a.attributes.slug}`)
return [...projectRoutes, ...articleRoutes]
}

View File

@ -1,4 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useRuntimeConfig } from '#app'
interface CompanyLogo { interface CompanyLogo {
url: string url: string
@ -78,6 +79,32 @@ interface Customer {
projects: CustomerProject[] projects: CustomerProject[]
} }
interface NewsArticle {
id: number
header: string
teaser: string
content: any
slug: string
image?: CompanyLogo | null
testfield?: string
SEO?: SEO | null
author?: {
id: number
name: string
// optional: weitere Felder aus dem Team-Modell
} | null
}
interface ContactData {
name: string;
email?: string;
phone?: string;
company?: string;
message: string;
language: string;
}
export const useMainStore = defineStore('main', { export const useMainStore = defineStore('main', {
state: () => ({ state: () => ({
menuOpen: false, menuOpen: false,
@ -88,6 +115,7 @@ export const useMainStore = defineStore('main', {
pages: [] as Page[], pages: [] as Page[],
customers: [] as Customer[], customers: [] as Customer[],
projects: [] as CustomerProject[], projects: [] as CustomerProject[],
articles: [] as NewsArticle[],
dataFetched: false, dataFetched: false,
loading: false, loading: false,
error: null as { message: string; stack?: string } | null, error: null as { message: string; stack?: string } | null,
@ -114,6 +142,8 @@ export const useMainStore = defineStore('main', {
}, },
getProjectByLink: (state) => (link: string) => getProjectByLink: (state) => (link: string) =>
state.projects.find(project => project.link === link), state.projects.find(project => project.link === link),
getArticleBySlug: (state) => (slug: string) =>
state.articles.find((a) => a.slug === slug),
}, },
actions: { actions: {
@ -133,6 +163,41 @@ export const useMainStore = defineStore('main', {
this.screenWidth = width this.screenWidth = width
}, },
// SEND CONTACT REQUEST TO STRAPI
async sendContactRequestToCMS(contactData: ContactData): Promise<void> {
const router = useRouter()
const config = useRuntimeConfig()
try {
const response = await axios.post(
`${config.public.cmsBaseUrl}/api/contacts`,
{
data: {
name: contactData.name,
email: contactData.email,
phone: contactData.phone,
company: contactData.company,
text: contactData.message,
page: router.currentRoute.value.fullPath,
language: contactData.language,
},
},
{
headers: {
Authorization: `Bearer ${config.public.cmsToken}`,
'Content-Type': 'application/json',
},
}
)
} catch (error) {
console.error('Fehler beim Senden der Kontaktanfrage:', error)
throw error
}
},
// FETCH ALL THE DATA FROM STRAPI
async fetchInitialData() { async fetchInitialData() {
if (this.dataFetched) return if (this.dataFetched) return
@ -140,7 +205,7 @@ export const useMainStore = defineStore('main', {
const { public: cfg } = useRuntimeConfig() const { public: cfg } = useRuntimeConfig()
try { try {
const [companyRes, pagesRes, customersRes, projectsRes] = await Promise.all([ const [companyRes, pagesRes, customersRes, projectsRes, articlesRes] = 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,6 +218,9 @@ export const useMainStore = defineStore('main', {
$fetch(`${cfg.cmsBaseUrl}/api/references?populate=projectImages,Technologien,customer&sort=launchDate:desc`, { $fetch(`${cfg.cmsBaseUrl}/api/references?populate=projectImages,Technologien,customer&sort=launchDate:desc`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` }, headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}), }),
$fetch(`${cfg.cmsBaseUrl}/api/newsarticels?populate=image,SEO,author&locale=all`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
]) ])
this.companyinfo = companyRes.data?.attributes ?? companyRes this.companyinfo = companyRes.data?.attributes ?? companyRes
@ -239,6 +307,44 @@ export const useMainStore = defineStore('main', {
} }
}) })
this.articles = articlesRes.data.map((item: any) => {
const a = item.attributes
return {
id: item.id,
header: a.header,
teaser: a.teaser,
content: a.content,
slug: a.slug,
testfield: a.testfield,
image: a.image?.data
? {
url: a.image.data.attributes.url,
alternativeText: a.image.data.attributes.alternativeText,
}
: null,
SEO: a.SEO
? {
pageTitle: a.SEO.pageTitle,
seoDescription: a.SEO.seoDesicription,
seoKeywords: a.SEO.seoKeywords,
type: a.SEO.type,
seoImage: a.SEO.seoImage?.data
? {
url: a.SEO.seoImage.data.attributes.url,
alternativeText: a.SEO.seoImage.data.attributes.alternativeText,
}
: null,
}
: null,
author: a.author?.data
? {
id: a.author.data.id,
name: a.author.data.attributes.name,
}
: null,
}
})
this.dataFetched = true this.dataFetched = true
} catch (err) { } catch (err) {
const e = err as Error const e = err as Error