This commit is contained in:
Sabrina Hennrich 2025-05-16 13:22:13 +02:00
commit fb73b7d2b5
91 changed files with 21580 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
18.20.6

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

34
app.vue Normal file
View File

@ -0,0 +1,34 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
import { useMainStore } from '@/stores/main'
const mainStore = useMainStore()
// Scroll- und Resize-Listener in den Lifecycle-Hooks registrieren
onMounted(() => {
window.addEventListener('scroll', handleScroll)
window.addEventListener('resize', handleResize)
// Bildschirmbreite direkt beim Start setzen:
handleResize()
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('resize', handleResize)
})
// Event-Handler
const handleScroll = () => {
mainStore.setScrollPosition(window.scrollY)
}
const handleResize = () => {
mainStore.setScreenWidth(window.innerWidth)
}
</script>

Binary file not shown.

BIN
assets/fonts/ColabThi.otf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,43 @@
Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 97.179 70.974" width="97.179pt" height="70.974pt"><defs><clipPath id="_clipPath_3mH8LEF1nCfoagEEbjrVXCwHhpsQbdCd"><rect width="97.179" height="70.974"/></clipPath></defs><g clip-path="url(#_clipPath_3mH8LEF1nCfoagEEbjrVXCwHhpsQbdCd)"><path d=" M 73.425 50.155 C 87.046 45.039 97.179 39.135 97.179 28.863 C 97.179 12.933 75.407 0 48.59 0 C 21.772 0 0 12.933 0 28.863 C 0 38.582 7.025 44.064 19.436 49.295 C 26.274 55.833 26.651 63.073 20.516 70.974 L 74.504 70.974 C 64.786 62.772 64.443 55.847 73.425 50.155 Z " fill="rgb(255,255,255)"/></g></svg>

After

Width:  |  Height:  |  Size: 752 B

View File

@ -0,0 +1,9 @@
body.high-contrast
background-color: black
color: white
html
--text-size: 16px
body
font-size: var(--text-size)

View File

@ -0,0 +1,262 @@
// Mixin für das Grid-System
=col($size)
flex: 0 0 $size
max-width: $size
// Dynamische Spalten (1 bis 12)
@for $i from 1 through 12
.col-#{$i}
+col(percentage($i / 12))
// Flexbox-Ausrichtungen
=flex-align($align-items, $justify-content)
display: flex
align-items: $align-items
justify-content: $justify-content
.d-flex
display: flex
.d-flex-center
+flex-align(center, center)
.d-flex-start
+flex-align(flex-start, flex-start)
.d-flex-end
+flex-align(flex-end, flex-end)
.d-flex-between
+flex-align(center, space-between)
// Spacing Mixins (Margin und Padding)
=spacing($type, $size)
#{$type}: $size
=m($size)
+spacing(margin, $size)
=mt($size)
+spacing(margin-top, $size)
=mb($size)
+spacing(margin-bottom, $size)
=p($size)
+spacing(padding, $size)
=pt($size)
+spacing(padding-top, $size)
=pb($size)
+spacing(padding-bottom, $size)
// Margin Klassen
.m-0
+m(0)
.m-1
+m(0.25rem)
.m-2
+m(0.5rem)
.m-3
+m(0.75rem)
.m-4
+m(1rem)
.m-5
+m(1.25rem)
.m-6
+m(1.5rem)
// Margin Top Klassen
.mt-0
+mt(0)
.mt-1
+mt(0.25rem)
.mt-2
+mt(0.5rem)
.mt-3
+mt(0.75rem)
.mt-4
+mt(1rem)
.mt-5
+mt(1.25rem)
.mt-6
+mt(1.5rem)
// Margin Bottom Klassen
.mb-0
+mb(0)
.mb-1
+mb(0.25rem)
.mb-2
+mb(0.5rem)
.mb-3
+mb(0.75rem)
.mb-4
+mb(1rem)
.mb-5
+mb(1.25rem)
.mb-6
+mb(1.5rem)
// Padding Klassen
.p-0
+p(0)
.p-1
+p(0.25rem)
.p-2
+p(0.5rem)
.p-3
+p(0.75rem)
.p-4
+p(1rem)
.p-5
+p(1.25rem)
.p-6
+p(1.5rem)
// Padding Top Klassen
.pt-0
+pt(0)
.pt-1
+pt(0.25rem)
.pt-2
+pt(0.5rem)
.pt-3
+pt(0.75rem)
.pt-4
+pt(1rem)
.pt-5
+pt(1.25rem)
.pt-6
+pt(1.5rem)
// Padding Bottom Klassen
.pb-0
+pb(0)
.pb-1
+pb(0.25rem)
.pb-2
+pb(0.5rem)
.pb-3
+pb(0.75rem)
.pb-4
+pb(1rem)
.pb-5
+pb(1.25rem)
.pb-6
+pb(1.5rem)
// Text-Ausrichtungen
=text-align($align)
text-align: $align
.text-left
+text-align(left)
.text-center
+text-align(center)
.text-right
+text-align(right)
// Vertikale Ausrichtung
.align-items-start
align-items: flex-start
.align-items-center
align-items: center
.align-items-end
align-items: flex-end
.align-items-baseline
align-items: baseline
.align-items-stretch
align-items: stretch
// Horizontale Ausrichtung
.justify-content-start
justify-content: flex-start
.justify-content-center
justify-content: center
.justify-content-end
justify-content: flex-end
.justify-content-between
justify-content: space-between
.justify-content-around
justify-content: space-around
.justify-content-evenly
justify-content: space-evenly
// Globales Box-Sizing für alle Elemente
*
box-sizing: border-box
// Row-Klasse für das Grid-System
.row
display: flex
flex-wrap: wrap
margin-left: -15px
margin-right: -15px
width: 100%
// Optional: Unterstützung für no-gutters (keine Abstände zwischen Spalten)
&.no-gutters
margin-left: 0
margin-right: 0
> .col,
> [class*="col-"]
padding-left: 0
padding-right: 0
// Standard-Padding für Spalten (Gutter)
[class*="col-"]
padding-left: 15px
padding-right: 15px
// Mixin für das Grid-System
=col($size)
flex: 0 0 $size
width: $size
max-width: $size
// Dynamische Spalten (1 bis 12) - Standardbreiten für große Bildschirme
@for $i from 1 through 12
.col-#{$i}
+col(percentage($i / 12))
// Breakpoints jetzt mit min-width
$breakpoints: (lg: 1200px, md: 992px, sm: 768px)
=breakpoint($size)
@media (min-width: $size)
@content
=col-responsive($col, $size)
@each $key, $value in $breakpoints
+breakpoint($value)
.col-#{$key}-#{$col}
flex: 0 0 (100% / 12 * $col)
width: (100% / 12 * $col)
max-width: (100% / 12 * $col)
// Responsive Spalten
@for $i from 1 through 12
+col-responsive($i, lg)
+col-responsive($i, md)
+col-responsive($i, sm)
// Responsive Stacking für kleinere Bildschirme
@media (max-width: 992px)
[class*="col-"]
flex: 0 0 100%
width: 100%
max-width: 100%

187
assets/styles/main.sass Normal file
View File

@ -0,0 +1,187 @@
$primaryColor: #67caac
$pink: #CA6785
$purple: #983553
$yellow: #F0AE48
$darkgrey: #373737
$lightgrey: #f9f9f9
$beige: #EEEBE5
// BREAKPOINTS like Bootstrap
$breakPointSM: 576px
$breakPointMD: 768px
$breakPointLG: 992px
$breakPointXL: 1200px
$breakPointXXL: 1400px
@font-face
font-family: 'Mainfont'
src: url('@/assets/fonts/montserrat/Montserrat-Light.otf') format("opentype")
font-weight: normal
@font-face
font-family: 'Mainfont-Bold'
src: url('@/assets/fonts/montserrat/Montserrat-Medium.otf') format("opentype")
font-weight: normal
@font-face
font-family: 'Comfortaa'
src: url('@/assets/fonts/Comfortaa-Light.ttf') format("truetype")
font-weight: normal
@font-face
font-family: 'Comfortaa-bold'
src: url('@/assets/fonts/Comfortaa-Bold.ttf') format("truetype")
font-weight: normal
@font-face
font-family: 'Typewriter'
src: url('@/assets/fonts/JMH_Typewriter.ttf') format("truetype")
font-weight: normal
$fontSizeRoot: 18px
$fontSizeLarge: calc(1.2rem + 2vw)
$fontSizeMedium: calc(1.2rem + .8vw)
$fontSizeNormal: 1.2rem
$fontSizeSmall: calc(.9rem + .2vw)
$loopShape: 51% 49% 52% 48% / 53% 38% 62% 47%
/* Varianten für die Shape */
$loopShape1: 51% 49% 52% 48% / 53% 38% 62% 47% // Original
$loopShape2: 49% 51% 48% 52% / 38% 53% 47% 62% // Horizontal gespiegelt
$loopShape3: 52% 48% 51% 49% / 62% 47% 53% 38% // Vertikal gespiegelt
$loopShape4: 48% 52% 49% 51% / 47% 62% 38% 53% // Horizontal & vertikal gespiegelt
$loopShape5: 50% 50% 53% 47% / 55% 40% 60% 45% // Leichte Variation für Abwechslung
$innerShadow: inset 0 0 15px rgba(255, 255, 255, 0.5)
@keyframes bubble-wobble
0%, 100%
transform: scale(1) translate(0, 0)
25%
transform: scale(1.05) translate(2px, -4px)
50%
transform: scale(1.1) translate(4px, -1px)
75%
transform: scale(1.1) translate(4px, 2px)
@keyframes gradient-animation
0%
background-position: 0% 0%
50%
background-position: 50% 50%
100%
background-position: 100% 100%
@keyframes blobIn
0%
transform: scale(0)
opacity: 0
60%
transform: scale(1.2)
opacity: 1
100%
transform: scale(1)
opacity: 1
body
overflow-x: hidden
font-family: 'Mainfont', Arial
margin: 0
padding: 0
.container-5
width: 90%
margin: auto 5%
.container-10
width: 80%
margin: auto 10%
.fade-enter-active, .fade-leave-active
transition: opacity 1.2s ease
.fade-enter-from, .fade-leave-to
opacity: 0
*:focus
outline: 1px solid transparent
/*.router-link-active
position: relative
text-decoration: none
&::after
content: ''
position: absolute
bottom: -4px
left: -10%
width: 110%
height: 4px
background: linear-gradient(to right, transparent, $pink, transparent)
transform: rotate(-1deg) */
// Page Route transition
.fade-enter-active,
.fade-leave-active
transition: opacity 1s, transform 1s
.fade-enter-from
transform: translateY(200px)
opacity: 0
.fade-enter-to
transform: translateY(0)
opacity: 1
.fade-leave-from
transform: translateY(0)
opacity: 1
.fade-leave-to
transform: translateY(200px)
opacity: 0
// +++++++++++++++++
// ANIME Animations ++++
// +++++++++++++++++
// Basiszustand
.anime
opacity: 0
transform: translateY(20px)
transition: opacity 0.5s ease, transform 0.5s ease
// Sichtbarer Zustand
.visible
opacity: 1
transform: translateY(0)
// Fade-In-Animation
.anime_fadeIn.visible
animation: fadeIn 3s ease forwards
@keyframes fadeIn
from
opacity: 0
to
opacity: 1
// Pop-In-Animation
.anime_popIn.visible
animation: popIn 0.5s ease forwards
@keyframes popIn
from
opacity: 0
transform: scale(0.8)
to
opacity: 1
transform: scale(1)

View File

@ -0,0 +1,276 @@
<template>
<div class="accessabilityBox" :class="[{ open },{ mobile: screenWidth < 1350, desk: screenWidth >= 1350 }]">
<div class="icon-wrapper" :class="{ open }" @click="toggleOpen">
<div class="icon">
<svg>
<use xlink:href="/assets/icons/collection.svg#accessibility"/>
</svg>
</div>
<span v-if="open">{{ $t('accessibilitySettings') }}</span>
<button v-if="open" class="close-button" @click.stop="open = false">
</button>
</div>
<transition name="slide">
<div v-if="open" class="options">
<div
class="fontsize"
:class="{ active: isLargeFont }"
@click="toggleFontsize"
>
<UIcon name="radix-icons:font-size" class="size-12" />
<p>{{ $t('changeFontSize') }}</p>
</div>
<div
class="contrast"
:class="{ active: isHighContrast }"
@click="toggleContrast"
>
<UIcon name="radix-icons:half-2" class="size-12" />
<p>{{ $t('increaseContrast') }}</p>
</div>
<div
class="greyscale"
:class="{ active: isGreyscale }"
@click="toggleGreyscale"
>
<UIcon name="radix-icons:blending-mode" class="size-12" />
<p>{{ $t('greyscale') }}</p>
</div>
<div
class="hideImages"
:class="{ active: isHideImages }"
@click="toggleHideImages"
>
<UIcon name="radix-icons:image" class="size-12" />
<p>{{ $t('hideImages') }}</p>
</div>
<div
class="borderFocus"
:class="{ active: isBorderFocus }"
@click="toggleBorderFocus"
>
<UIcon name="radix-icons:box" class="size-12" />
<p>{{ $t('borderFocus') }}</p>
</div>
<div
class="linksVisible"
:class="{ active: isLinksVisible }"
@click="toggleLinksVisible"
>
<UIcon name="radix-icons:link-1" class="size-12" />
<p>{{ $t('showLinks') }}</p>
</div>
<p class="apFooter">{{ $t('infoAccessibility') }}</p>
</div>
</transition>
</div>
</template>
<script setup>
import { ref } from 'vue'
const open = ref(false)
import { useMainStore } from '@/stores/main'
const mainStore = useMainStore()
const screenWidth = computed(() => mainStore.screenWidth)
// Schriftgröße
const isLargeFont = ref(false)
const toggleFontsize = () => {
isLargeFont.value = !isLargeFont.value
document.documentElement.style.setProperty('--text-size', isLargeFont.value ? '20px' : '16px')
}
// Kontrast
const isHighContrast = ref(false)
const toggleContrast = () => {
isHighContrast.value = !isHighContrast.value
document.body.classList.toggle('high-contrast', isHighContrast.value)
}
// Graustufen
const isGreyscale = ref(false)
const toggleGreyscale = () => {
isGreyscale.value = !isGreyscale.value
document.body.classList.toggle('greyscale', isGreyscale.value)
}
// Bilder ausblenden
const isHideImages = ref(false)
const toggleHideImages = () => {
isHideImages.value = !isHideImages.value
document.body.classList.toggle('hide-images', isHideImages.value)
}
// Fokusrahmen aktivieren
const isBorderFocus = ref(false)
const toggleBorderFocus = () => {
isBorderFocus.value = !isBorderFocus.value
document.body.classList.toggle('border-focus', isBorderFocus.value)
}
// Links hervorheben
const isLinksVisible = ref(false)
const toggleLinksVisible = () => {
isLinksVisible.value = !isLinksVisible.value
document.body.classList.toggle('show-links', isLinksVisible.value)
}
// Box öffnen/schließen
const toggleOpen = () => {
open.value = !open.value
}
</script>
<style lang="sass">
.accessabilityBox
position: relative
display: block
background: white
border: 1px solid #ccc
border-bottom-left-radius: 1rem
padding: 0
cursor: pointer
font-family: sans-serif
overflow: hidden
user-select: none
touch-action: manipulation
width: 3.5rem
height: 3.5rem
transition: all 0.4s ease
z-index: 14
&.mobile
transform-origin: bottom right
&.open
width: 100%
height: 80%
max-width: 500px
border-top-left-radius: 1rem
.icon-wrapper
width: 100%
display: flex
justify-content: center
align-items: center
background: white
border-bottom: 1px solid #ccc
transition: all 0.6s ease
height: 3.5rem
position: relative
.icon
width: 1.2rem
display: inline-block
text-align: center
svg
width: 100%
fill: $darkgrey
max-height: 1.5rem
&.open
background: $darkgrey
color: white
font-size: 1.2rem
padding: .8rem 1rem
height: 2.5rem
border-top-left-radius: 1rem
border-top-right-radius: 1rem
justify-content: flex-start
gap: 0.8rem
.main-icon
width: 2.5rem
height: 2.5rem
transition: all 0.6s ease
.close-button
position: absolute
top: 0.25rem
right: .5rem
background: white
border: none
font-size: 1.5rem
cursor: pointer
color: black
border-radius: 50%
width: 2rem
height: 2rem
display: flex
align-items: center
justify-content: center
transition: background 0.3s
&:hover
background-color: $lightgrey
.apFooter
position: absolute
bottom: -1rem
left: 0
width: 100%
background-color: $darkgrey
color: white
padding: .5rem 1rem
border-bottom-left-radius: 1rem
.options
display: flex
flex-wrap: wrap
justify-content: center
margin-bottom: 2rem
gap: 1rem
padding: 2rem
div
cursor: pointer
transition: all 0.8s ease
font-size: 1.2rem
display: flex
flex-direction: column
align-items: center
justify-content: center
width: 8rem
height: 8rem
background: white
border: 3px solid $lightgrey
border-radius: 1rem
text-align: center
p
margin-top: 0.5rem
font-size: 0.9rem
color: $darkgrey
.size-12
width: 3rem
height: 3rem
&:hover
//transform: scale(1.1)
background: $lightgrey
&.active
border-color: $pink
&:hover
background-color: white
.slide-enter-active, .slide-leave-active
transition: all 0.3s ease
.slide-enter-from, .slide-leave-to
opacity: 0
transform: translateY(-10px)
</style>

219
components/Accordion.vue Normal file
View File

@ -0,0 +1,219 @@
<template>
<div class="myAccordion">
<div v-for="(item, index) in items" :key="index" class="accordion-item">
<div
class="accordion-header"
:ref="el => setHeaderRef(el, index)"
@click="toggleSection(index)"
>
<div class="accordion-title">{{ item.title }}</div>
<div class="accordion-toggle" :class="{ open: openIndex === index }">
<span></span>
<span></span>
</div>
</div>
<div
class="accordion-content"
:ref="el => setContentRef(el, index)"
:style="{ maxHeight: openIndex === index ? `${contentHeights[index]}px` : '0px' }"
>
<p><span v-html="htmlContent(item.content)"></span></p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue';
import { useHtmlConverter } from '../composables/useHTMLConverter';
const { convertToHTML } = useHtmlConverter();
const htmlContent = (data) => {
return convertToHTML(data); // Nutze die convertToHTML Funktion der Composable
};
// Props für die Items
const props = defineProps({
items: {
type: Array,
required: true,
default: () => [],
},
});
// Zustand für das geöffnete Element
const openIndex = ref(null);
// Referenzen für die Inhalte
const contentRefs = ref([]);
// Höhen der Inhalte
const contentHeights = ref([]);
// Referenzen für die Header-Elemente
const headerRefs = ref([]);
// Funktion zum Umschalten des geöffneten Abschnitts
const toggleSection = async (index) => {
// Umschalten des offenen Indexes
openIndex.value = openIndex.value === index ? null : index;
// Wenn ein neuer Abschnitt geöffnet wurde
if (openIndex.value !== null) {
await nextTick(); // Warten, bis das DOM aktualisiert wurde
setTimeout(() => {
const header = headerRefs.value[openIndex.value]; // Header des offenen Elements
const fixedHeaderHeight = document.querySelector('.fixed-header')?.offsetHeight || 0;
if (header) {
const topPosition = header.getBoundingClientRect().top + window.scrollY - fixedHeaderHeight - 20; // 20px Puffer
// Scrollen zur berechneten Position
window.scrollTo({
top: topPosition > 0 ? topPosition : 0, // Verhindert negatives Scrollen
behavior: 'smooth',
});
}
}, 100); // Kleiner Timeout für das reibungslose Scrollen
}
};
// Setze die Referenzen dynamisch
const setContentRef = (el, index) => {
if (el) {
contentRefs.value[index] = el;
}
};
const setHeaderRef = (el, index) => {
if (el) {
headerRefs.value[index] = el;
}
};
// Berechne die Höhen der Inhalte
const calculateHeights = () => {
contentHeights.value = contentRefs.value.map((el) => {
if (!el) return 0;
const additionalHeight = 50; // Fester Wert für oben und unten
return el.scrollHeight + additionalHeight; // Dynamische Höhe + fester Wert
});
};
// Initialisiere die Berechnung nach dem Mounten
onMounted(async () => {
contentRefs.value = Array.from({ length: props.items.length });
headerRefs.value = Array.from({ length: props.items.length });
await nextTick();
calculateHeights();
});
// Aktualisiere die Höhen, wenn sich die Items ändern
watch(
() => props.items,
async () => {
contentRefs.value = Array.from({ length: props.items.length });
headerRefs.value = Array.from({ length: props.items.length });
await nextTick();
calculateHeights();
}
);
</script>
<style lang="scss">
.myAccordion {
.accordion-item {
border-bottom: 1px solid #ccc;
&:last-of-type {
border-bottom: none;
}
.accordion-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding: 1rem;
font-family: 'Mainfont-Bold';
background-color: white;
transition: background-color 0.3s ease;
&:hover {
background-color: lighten(#EEEBE5, 6%);
}
.accordion-title {
flex: 1; // Nimmt den freien Platz ein
text-align: left; // Links ausgerichtet
font-weight: bold;
font-size: 1rem;
margin-right: 1rem;
}
.accordion-toggle {
position: relative;
flex-shrink: 0; // Verhindert, dass das Icon skaliert wird
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
span {
position: absolute;
width: 15px; // Kürzere Linien für saubere Spitzen
height: 2px;
background-color: #333;
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease;
top: 8px;
&:first-child {
transform: rotate(135deg); // Linie 1: Oben links zur Mitte
left: 5px; // Leicht nach links verschoben
}
&:last-child {
transform: rotate(-135deg); // Linie 2: Oben rechts zur Mitte
left: -5px; // Leicht nach rechts verschoben
}
}
&.open {
span:first-child {
transform: rotate(45deg); // Linie 1: Teil des "X"
left: 0; // Zentriert
}
span:last-child {
transform: rotate(-45deg); // Linie 2: Teil des "X"
left: 0; // Zentriert
}
}
}
}
.accordion-content {
overflow: hidden;
transition: max-height 0.3s ease-out;
padding: 0 1rem;
background-color: white;
h3 {
font-size: 1rem;
line-height: 1.4rem;
margin-bottom: .5rem;
}
p {
margin: 1rem 0.5rem;
font-size: 1rem;
}
}
}
}
</style>

116
components/Breadcrumbs.vue Normal file
View File

@ -0,0 +1,116 @@
<template>
<nav class="breadcrumbs" aria-label="Breadcrumb" v-if="breadcrumbs && breadcrumbs.length">
<ul>
<router-link to="/">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path d="M575.8 255.5c0 18-15 32.1-32 32.1l-32 0 .7 160.2c0 2.7-.2 5.4-.5 8.1l0 16.2c0 22.1-17.9 40-40 40l-16 0c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1L416 512l-24 0c-22.1 0-40-17.9-40-40l0-24 0-64c0-17.7-14.3-32-32-32l-64 0c-17.7 0-32 14.3-32 32l0 64 0 24c0 22.1-17.9 40-40 40l-24 0-31.9 0c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2l-16 0c-22.1 0-40-17.9-40-40l0-112c0-.9 0-1.9 .1-2.8l0-69.7-32 0c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"/>
</svg>
</router-link>
<li v-for="(crumb, index) in breadcrumbs" :key="index">
<router-link v-if="index < breadcrumbs.length - 1" :to="crumb.to">
{{ crumb.label }}
</router-link>
<span v-else>{{ crumb.label }}</span>
</li>
</ul>
</nav>
</template>
<script>
import { useI18n } from 'vue-i18n'
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
const pathArray = pathWithoutLang.split('/').filter(p => p)
let path = ''
return pathArray.map(segment => {
path += '/' + segment
return {
label: this.formatLabel(segment),
to: `/${locale}${path}` // Sprachprefix im Link wieder einfügen
}
})
}
},
methods: {
formatLabel(segment) {
return segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ')
}
}
}
</script>
<style lang="sass">
.breadcrumbs
position: fixed
top: 22vh
left: 0
text-align: left
padding: 1rem .25rem 1rem .5rem
//min-width: 200px
background-color: rgba(white, .98)
border: 1px solid $lightgrey
writing-mode: vertical-rl
transform: rotate(180deg)
//border-bottom-left-radius: .65rem
border-top-left-radius: .8rem
border-bottom-left-radius: .8rem
text-transform: uppercase
letter-spacing: .05rem
z-index: 22
ul
display: flex
flex-wrap: wrap
list-style: none
padding: 0
margin: 0
gap: 0.5rem
cursor: pointer
li
display: flex
align-items: center
color: $primaryColor
font-size: .7rem
svg
width: .8rem
height: .8rem
transform: rotate(90deg)
margin-bottom: .35rem
transition: .3s
path
fill: darken($lightgrey, 20%)
&:hover
transform: scale(1.3) rotate(90deg)
path
fill: darken($lightgrey, 30%)
&::after
content: '>'
margin: 0 0.5rem
color: $pink
&:last-child::after
content: ''
a
color: #007BFF
text-decoration: none
&:hover
transform: scale(1.2)
span
font-weight: bold
</style>

446
components/ContactForm.vue Normal file
View File

@ -0,0 +1,446 @@
<template>
<div
class="contactBubble"
:class="{ active: isContactBubbleOpen }"
aria-hidden="false"
:aria-expanded="isContactBubbleOpen"
aria-labelledby="controlIcon"
role="dialog"
>
<svg
@click="toggleContactBubble"
id="controlIcon"
tabindex="0"
role="button"
aria-label="Toggle contact form"
>
<use :xlink:href="`/assets/icons/collection.svg#${isContactBubbleOpen ? 'times' : 'talk'}`"></use>
</svg>
<div
class="contactContainer"
v-show="isContactBubbleOpen"
role="form"
aria-labelledby="contactTitle"
>
<div class="row left m-2">
<div class="col-md-6" id="hintBox">
<img class="mobileAspBox" v-if="screenWidth <= 768" :src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url" alt="Ansprechpartner Sabrina Hennrich">
<h2 id="contactTitle">{{ $t('contactForm.yourcontact2us') }}</h2>
<p class="my-4">
<svg aria-hidden="true">
<use xlink:href="/assets/icons/collection.svg#phone"></use>
</svg>
<span>{{ companyinfo.phone }}</span>
</p>
<div class="pt-3" v-if="screenWidth > 768">
<h3>{{ $t('contactForm.ourOffice') }}</h3>
<p class="address">
{{ companyinfo.company }}<br>{{ companyinfo.street }} <br>{{ companyinfo.postalcode }} {{ companyinfo.city }}
</p>
<p class="aspProf">{{ $t('contactForm.yourcontactperson') }} <b>Sabrina Hennrich</b></p>
<div class="aspBox"><img :src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url" alt="Ansprechpartner Sabrina Hennrich"></div>
</div>
</div>
<div class="col-md-6">
<div v-if="!formSent">
<div class="form-group">
<label for="name">{{ $t('contactForm.name') }}</label>
<input
id="name"
type="text"
name="name"
v-model="form.name"
@blur="validateName"
aria-required="true"
autocomplete="name"
>
<span v-if="errors.name" class="error">{{ errors.name }}</span>
</div>
<div class="form-group">
<label for="email">{{ $t('contactForm.email') }}</label>
<input
id="email"
type="email"
name="email"
v-model="form.email"
@blur="validateEmail"
autocomplete="email"
>
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>
<div class="form-group">
<label for="phone">{{ $t('contactForm.phone') }}</label>
<input
id="phone"
type="tel"
name="phone"
v-model="form.phone"
@blur="validatePhone"
autocomplete="tel"
>
<span v-if="errors.phone" class="error">{{ errors.phone }}</span>
</div>
<div class="form-group">
<label for="message">{{ $t('contactForm.message') }}</label>
<textarea
id="message"
name="message"
class="mt-4"
v-model="form.message"
></textarea>
</div>
<p class="smallText">
<span class="check"></span>
{{ $t('contactForm.privacyInfotextBeforeLink') }}
<NuxtLinkLocale
to="/privacy"
:aria-label="$t('privacy')"
>
{{ $t('contactForm.privacyInfotextLinkText') }}
</NuxtLinkLocale>
</p>
<button
type="submit"
:aria-label="$t('contactForm.sendMessage')"
@click="submitForm"
class="pinkBtn"
>
{{ $t('contactForm.sendMessage') }}
</button>
</div>
<div v-else class="mt-5 thx">
<h3 class="pt-5">{{ $t('contactForm.confirmation.thx') }}</h3>
<p>{{ $t('contactForm.confirmation.info') }}</p>
<p>{{ $t('contactForm.confirmation.salutation') }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useMainStore } from '@/stores/main';
import { ref, reactive, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
// i18n Setup
const { t } = useI18n();
// Zugriff auf den Pinia-Store
const mainStore = useMainStore();
const { companyinfo } = storeToRefs(mainStore);
const config = useRuntimeConfig();
const cmsUrl = computed(() => config.public.cmsBaseUrl);
const isContactBubbleOpen = computed(() => mainStore.contactBoxOpen);
const toggleContactBubble = () => mainStore.toggleContactBubble();
const screenWidth = computed(() => mainStore.screenWidth);
const formSent = ref(false);
// Formular- und Fehlerzustand
const form = reactive({
name: '',
email: '',
phone: '',
message: '',
company: '',
language: 'de' // Beispielhaft festgelegt
});
const errors = reactive({
name: null as string | null,
email: null as string | null,
phone: null as string | null
});
// Validierungsfunktionen
const validateName = () => {
errors.name = form.name ? null : t('contactForm.validation.nameRequired');
};
const validateEmail = () => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!form.email && !form.phone) {
errors.email = t('contactForm.validation.emailOrPhoneRequired');
} else if (form.email && !emailRegex.test(form.email)) {
errors.email = t('contactForm.validation.invalidEmail');
} else {
errors.email = null;
}
};
const validatePhone = () => {
const phoneRegex = /^[+]?([0-9\- ]{6,15})$/;
if (!form.email && !form.phone) {
errors.phone = t('contactForm.validation.emailOrPhoneRequired');
} else if (form.phone && !phoneRegex.test(form.phone)) {
errors.phone = t('contactForm.validation.invalidPhone');
} else {
errors.phone = null;
}
};
const validateForm = () => {
validateName();
validateEmail();
validatePhone();
};
const submitForm = async () => {
validateForm();
if (!errors.name && !errors.email && !errors.phone) {
try {
await mainStore.sendContactRequestToCMS({
name: form.name,
email: form.email,
phone: form.phone,
message: form.message,
company: form.company,
language: navigator.language,
page: window.location.pathname
});
formSent.value = true;
setTimeout(() => {
formSent.value = false;
}, 5500);
resetForm();
} catch (error) {
console.error('Fehler beim Senden des Formulars:', error);
alert(t('contactForm.errorMessage'));
}
}
};
const resetForm = () => {
form.name = '';
form.email = '';
form.phone = '';
form.message = '';
form.company = '';
errors.name = null;
errors.email = null;
errors.phone = null;
};
</script>
<style lang="sass">
@keyframes awarePulse
0%
box-shadow: 0 0 0 0 rgba(202, 103, 133, 0.25)
50%
box-shadow: 0 0 0 25px rgba(202, 103, 133, 0.15)
100%
box-shadow: 0 0 0 40px rgba(202, 103, 133, 0)
.contactBubble
position: fixed
bottom: 3rem
right: 2rem
width: 80px
height: 80px
background-color: $pink
border-radius: $loopShape
color: white
font-size: 2rem
text-align: center
z-index: 100
cursor: pointer
transition: .8s
display: flex
flex-direction: column
align-items: center
justify-content: center
animation: awarePulse 1.8s infinite ease-out
@media(max-width: $breakPointMD)
bottom: 4vw
right: 4vw
.contactContainer
.thx
background-color: $lightgrey
border-radius: $loopShape
padding: 0 3rem 2rem
text-align: center
h3
font-size: 1.2rem
p
font-size: 1rem
#hintBox
padding-top: 3rem
h2
font-size: 1.8rem
@media(max-width: $breakPointMD)
text-align: center
padding-top: 1rem
svg
max-height: 60px
width: 80%
margin: 6rem 10%
h3
font-size: .8rem
font-family: 'Mainfont-Bold'
color: $pink
p
color: $darkgrey
font-size: 1.2rem
&.smallText
font-size: .8rem
&.address
font-size: .9rem
.check
font-size: 1.4rem
color: $pink
svg
width: 2rem
height: 2rem
fill: $darkgrey
margin-right: 1rem
max-height: 100px
@media(max-width: $breakPointSM)
margin-right: .5rem
width: 1.5rem
height: 1.5rem
a
color: $primaryColor
#controlIcon
position: absolute
bottom: 50%
left: 50%
transform: translate(-50%, 50%)
fill: white
height: auto
width: 3rem
height: 3rem
z-index: 101
&.active
height: 90vh
width: 90vw
background-color: rgba(lighten($beige, 8%), .98)
color: $darkgrey
display: flex
flex-direction: column
text-align: left
border: 1px solid $lightgrey
animation: none
box-shadow: 1px 1px 15px 2px $beige
@media(max-width: $breakPointMD)
border-radius: 0
height: 100%
width: 100%
right: 0
top: 0
#controlIcon
right: 5vw
left: auto
bottom: 4rem !important
#controlIcon
bottom: 3rem
fill: #888
width: 2rem
.mobileAspBox
width: 30vw
max-width: 90px
float: left
margin: 0 .5rem 1rem -2rem
border-radius: $loopShape
@media(max-width: $breakPointSM)
margin-right: .1rem
input, textarea
all: unset
position: relative
width: 100%
padding: .2rem .5rem .2rem .5rem
font-size: 1.2rem
background-color: transparent
border-top-right-radius: .3rem
border-top-left-radius: .3rem
border-bottom: 1px solid lighten($darkgrey, 40%)
border-left: none
border-right: none
border-top: none
margin-bottom: .5rem
margin-top: 0
&::placeholder
color: darken($beige, 8%)
font-size: 1.1rem
&:focus
outline: none // Entfernt den Standard-Fokusrahmen
border-bottom-color: $primaryColor // Ändert die Farbe der unteren Begrenzung
transition: border-color 0.3s ease-in-out // Smooth Transition beim Wechsel der Farbe
&:-webkit-autofill
background-color: transparent !important
color: inherit !important
-webkit-box-shadow: 0 0 0px 1000px transparent inset
box-shadow: 0 0 0px 1000px transparent inset
transition: background-color 5000s ease-in-out 0s
textarea
height: 20%
font-size: 1.1rem
button
font-size: 1.2rem
border: none
background-image: linear-gradient(to bottom right, lighten($pink, 10%), $pink)
padding: .5rem 1rem
border-radius: .8rem
color: white
.aspBox
width: 100%
img
width: 50%
max-width: 150px
border-radius: $loopShape
.aspProf
font-size: .8rem !important
margin-top: 1rem
width: 80%
color: darken($primaryColor, 20%) !important
b
font-family: 'Mainfont-Bold'
color: $darkgrey
margin-left: .2rem
font-size: .9rem
// Form-group Anpassungen
.form-group
position: relative
margin-bottom: .8rem
label
position: absolute
top: -.2rem
left: 0.5rem
font-size: 0.9rem
color: $primaryColor
pointer-events: none
transition: all 0.3s ease-in-out
input, textarea
padding-top: 1rem // Platz für das Label schaffen
input:focus + label, textarea:focus + label
top: -1rem
font-size: 0.8rem
color: $primaryColor
.error
color: #CC0000
font-size: .8rem
display: block
line-height: 1rem
</style>

61
components/FAQArea.vue Normal file
View File

@ -0,0 +1,61 @@
<template>
<section class="faq" id="faq">
<h3>{{ headline }}</h3>
<Accordion v-if="accordionItems.length" :items="accordionItems" />
<p v-else>Lade Daten...</p>
<div class="row mt-4">
<div class="col-md-6 mb-3">
<h4> Noch Fragen? </h4>
</div>
<div class="col-md-6">
<button @click.prevent="toggleContactBubble" role="button" class="pinkBtn">
{{ button }}
</button>
</div>
</div>
</section>
</template>
<script setup>
import { useMainStore } from '@/stores/main';
import { storeToRefs } from 'pinia';
import { computed, defineProps, defineAsyncComponent } from 'vue';
const props = defineProps({
pageLink: { type: String, required: true },
headline: { type: String, default: "Häufig gestellte Fragen (FAQs)" },
button: { type: String, default: "Sprechen Sie uns gerne an!" },
});
const mainStore = useMainStore();
const { pages } = storeToRefs(mainStore); // Wir holen die `pages` aus dem Pinia-Store
const toggleContactBubble = () => mainStore.toggleContactBubble();
// 🔹 **FAQs für die aktuelle Seite aus `pages` filtern**
const accordionItems = computed(() => {
const currentPage = pages.value?.find(page => page.pageLink === props.pageLink);
const faqsArray = Array.isArray(currentPage?.faqs.data) ? currentPage.faqs.data : []; // Sicherstellen, dass es ein Array ist
return faqsArray.map(faq => ({
title: faq.attributes.question,
content: faq.attributes.answer,
}));
});
</script>
<style lang="sass">
.faq
width: 80%
margin: 2rem auto
h3
font-size: 1.4rem
font-family: 'Mainfont-Bold'
h4
font-size: 1.4rem
font-family: 'Mainfont-Bold'
margin-top: .6rem
</style>

View File

@ -0,0 +1,112 @@
<template>
<div class="image-comparison-container">
<!-- "Vorher"-Bild liegt unten -->
<img class="before-image" :src="beforeImage" alt="Vorher-Bild">
<!-- "Nachher"-Bild oben, wird durch die Breite beschnitten -->
<div class="after-image-container" :style="{ width: sliderValue + '%' }">
<img class="after-image" :src="afterImage" alt="Nachher-Bild">
</div>
<!-- Der Slider -->
<input type="range" min="0" max="100" v-model="sliderValue" class="slider">
<!-- Die vertikale Trennlinie -->
<div class="slider-line" :style="{ left: sliderValue + '%' }"></div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
beforeImage: String,
afterImage: String
})
const sliderValue = ref(60) // Startposition in der Mitte
</script>
<style lang="sass">
.image-comparison-container
position: relative
width: 100%
height: auto
aspect-ratio: 12 / 9
user-select: none
overflow: hidden
.after-image,
.before-image
width: auto
height: 100%
object-fit: cover
display: block
.after-image-container
position: absolute
top: 0
left: 0
height: 100%
overflow: hidden
pointer-events: none
img
width: auto
height: 100%
object-fit: cover
/* Slider */
.slider
position: absolute
top: 50%
left: 0
width: 100%
height: 8px // Slider-Höhe anpassen
transform: translateY(-50%)
appearance: none
background: transparent
cursor: pointer
z-index: 10
-webkit-appearance: none
-moz-appearance: none
&::-webkit-slider-thumb
appearance: none
width: 20px // Breite des Schiebereglers
height: 20px // Höhe des Schiebereglers
border-radius: 50%
background: $pink
border: 1px solid #fff
cursor: pointer
z-index: 15
&::-moz-range-thumb
width: 20px
height: 20px
border-radius: 50%
background: $pink
border: 1px solid #fff
cursor: pointer
&::-ms-thumb
width: 20px
height: 20px
border-radius: 50%
background: $pink
border: 1px solid #fff
cursor: pointer
/* Vertikale Trennlinie */
.slider-line
position: absolute
top: 0
width: 4px
height: 100%
background: white
border-radius: 2px
z-index: 5
</style>

View File

@ -0,0 +1,85 @@
<template>
<div class="languageBox" @click="toggleOpen">
<div v-if="!open" class="current">{{ currentLanguage }}</div>
<transition name="slide">
<div v-if="open" class="options">
<span
v-for="localeItem in locales"
:key="localeItem.code"
:class="{ active: localeItem.code === locale }"
@click.stop="selectLanguage(localeItem.code)"
>
{{ localeItem.code }}
</span>
</div>
</transition>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const { locales, setLocale, locale } = useI18n()
const open = ref(false)
const toggleOpen = () => {
open.value = !open.value
}
const selectLanguage = (code) => {
setLocale(code)
open.value = false // nach Auswahl schließen
}
const currentLanguage = computed(() => locale.value.toUpperCase())
</script>
<style lang="sass">
.languageBox
position: relative
display: block
align-items: center
background: white
border: 1px solid #ccc
border-top-left-radius: 1rem
border-bottom-left-radius: 1rem
padding: 1rem .8rem 1rem 1rem
cursor: pointer
font-family: sans-serif
text-transform: uppercase
overflow: hidden
user-select: none
touch-action: manipulation
z-index: 14
.current
font-weight: bold
font-size: 1.2rem
.options
display: inline-flex
margin-left: .5rem
gap: 0.5rem
span
cursor: pointer
transition: all 0.3s ease
font-size: 1.1rem
&:hover
//font-weight: bold
font-size: 1.2rem
transform: scale(1.2)
&.active
font-weight: bold
font-size: 1.2rem
&:not(:last-child)::after
content: '|'
margin-left: 0.5rem
color: $pink
font-weight: normal
.slide-enter-active, .slide-leave-active
transition: all 0.3s ease
.slide-enter-from, .slide-leave-to
opacity: 0
transform: translateX(-10px)
</style>

View File

@ -0,0 +1,286 @@
<template>
<div>
<div class="banner-wrapper">
<!-- Obere Welle als SVG -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 500 20"
class="svgwavetop"
style="transform: scaleY(-1)"
>
<g clip-path="url(#_clipPath_5kVoellZ93LI5Lc2i2b27JZsraaBm0XM)">
<path
id="wave"
d=" M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z "
fill="#EEEBE5"
></path>
</g>
</svg>
<div class="box pb-5">
<div class="container">
<h2 class="pt-4 pb-3">{{ title }}</h2>
<!-- Marquee mit doppeltem Inhalt für endloses Scrollen -->
<div class="marquee marquee--hover-pause mt-5">
<ul class="marquee__content">
<li v-for="(item, index) in items" :key="index">
<NuxtLink
v-if="item.link"
:to="`/${link}/${item.link}`"
class="custLogoLink"
>
<img
:src="cmsUrl + getImageUrl(item)"
class="custLogo"
:style="{ height: logoHeight + 'px' }"
:alt="getAltText(item)"
/>
</NuxtLink>
<img
v-else
:src="cmsUrl + getImageUrl(item)"
class="custLogo"
:style="{ height: logoHeight + 'px' }"
:alt="getAltText(item)"
/>
</li>
</ul>
<ul aria-hidden="true" class="marquee__content">
<li v-for="(item, index) in items" :key="'duplicate-' + index">
<NuxtLink
v-if="item.link"
:to="`/${link}/${item.link}`"
class="custLogoLink"
>
<img
:src="cmsUrl + getImageUrl(item)"
class="custLogo"
:style="{ height: logoHeight + 'px' }"
:alt="getAltText(item)"
/>
</NuxtLink>
<img
v-else
:src="cmsUrl + getImageUrl(item)"
class="custLogo"
:style="{ height: logoHeight + 'px' }"
:alt="getAltText(item)"
/>
</li>
</ul>
</div>
</div>
</div>
<!-- Wellen am unteren Rand -->
<div class="waveBox">
<div id="waver">
<div class="waveWrapper waveAnimation">
<div class="waveWrapperInner bgTop">
<div
class="wave waveTop"
:style="{ backgroundImage: `url('${cmsUrl}/uploads/wave_top_8fe067e598.svg')` }"
></div>
</div>
<div class="waveWrapperInner bgMiddle">
<div
class="wave waveMiddle"
:style="{ backgroundImage: `url('${cmsUrl}/uploads/wave_middle_24d8a84a35.svg')` }"
></div>
</div>
<div class="waveWrapperInner bgBottom">
<div
class="wave waveBottom"
:style="{ backgroundImage: `url('${cmsUrl}/uploads/wave_bottom_6fc8184efb.svg')` }"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const runtimeConfig = useRuntimeConfig();
const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl)
// Props: title, items, logoHeight, und link (optional)
const props = defineProps({
items: {
type: Array,
required: true,
},
logoHeight: {
type: Number,
default: 50, // Standardhöhe in Pixel
},
title: {
type: String,
default: '',
},
link: {
type: String,
default: 'projekt', // Standardwert, wenn keine spezifische Seite angegeben wird
},
});
// Funktion zur Bestimmung des Bild-URLs basierend auf Typ des Items
const getImageUrl = (item) => {
if (item.logo) {
// Für Customers: Verwende das logo-Feld
return item.logo.data.attributes.url;
} else if (item.projectImages && item.projectImages.data.length > 0) {
// Für Projects: Verwende das erste Bild im projectImages-Feld
return item.projectImages.data[0].attributes.url;
}
return '';
};
// Funktion zur Ermittlung des Alt-Texts für das Bild
const getAltText = (item) => {
if (item.logo) {
return item.logo.data.attributes.alternativeText || item.company || '';
} else if (item.projectImages && item.projectImages.data.length > 0) {
return item.projectImages.data[0].attributes.alternativeText || item.projectTitle || '';
}
return '';
};
</script>
<style lang="sass">
.banner-wrapper
position: relative
svg
margin: 0 0 -3px 0
@media(max-width: $breakPointSM)
svg
margin: 0
.box
background-color: $beige
width: 100%
min-height: 50px
margin-top: -20px
h2
color: #333
font-size: 1.2rem
font-family: 'Mainfont-Bold'
.marquee
--gap: 1rem
position: relative
display: flex
overflow: hidden
user-select: none
gap: var(--gap)
ul
list-style-type: none
&:hover .marquee__content
animation-play-state: paused
.marquee__content
flex-shrink: 0
display: flex
justify-content: space-around
gap: var(--gap)
min-width: 100%
animation: scroll 30s linear infinite
li
&::before
display: none
@keyframes scroll
from
transform: translateX(0)
to
transform: translateX(calc(-100% - var(--gap)))
.custLogo
width: auto
max-width: 250px
height: 50px
margin: 0 3rem
filter: grayscale(100%)
transition: filter 0.3s ease
&:hover
filter: grayscale(0)
.waveBox
position: relative
height: 120px
#waver
display: block
position: absolute
left: 0
height: 120px
width: 100%
padding: 0
margin: 0
@keyframes move_wave
0%
transform: translateX(0) translateZ(0) scaleY(1)
50%
transform: translateX(-25%) translateZ(0) scaleY(0.55)
100%
transform: translateX(-50%) translateZ(0) scaleY(1)
.waveWrapper
overflow: hidden
position: absolute
left: 0
right: 0
bottom: 0
top: 0
margin: auto
.waveWrapperInner
position: absolute
width: 100%
overflow: hidden
height: 120px
top: 0
background-image: linear-gradient(to top, $beige 20%, $beige 80%)
@media (max-width: 1024px)
.waveWrapperInner
height: 50px
.bgTop
z-index: 15
opacity: 0.5
.bgMiddle
z-index: 10
opacity: 0.75
.bgBottom
z-index: 5
.wave
position: absolute
left: 0
width: 200%
height: 100%
background-repeat: repeat no-repeat
background-position: 0 bottom
transform-origin: center bottom
.waveTop
background-size: auto 100%
animation: move_wave 18s linear infinite
.waveMiddle
background-size: auto 100%
animation: move_wave 11s linear infinite
.waveBottom
background-size: auto 100%
animation: move_wave 15s linear infinite
</style>

View File

@ -0,0 +1,30 @@
<template>
<div id="settingsPanel" :class="{ mobile: screenWidth < 1350, desk: screenWidth >= 1350 }">
<LanguageBox />
<!--<AccessabilityBox />-->
</div>
</template>
<script setup>
import { useMainStore } from '@/stores/main'
const mainStore = useMainStore()
const screenWidth = computed(() => mainStore.screenWidth)
</script>
<style lang="sass">
#settingsPanel
position: absolute
top: 25vh
right: 0
display: flex
flex-direction: column
align-items: flex-end
gap: 0
width: auto
height: auto
z-index: 12
&.mobile
top: 45vh
</style>

View File

@ -0,0 +1,243 @@
<template>
<footer>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 500 20"
class="svgwavetop"
style="
transform: scaleY(-1) scaleX(-1) translateY(99%);
fill: rgba(38, 38, 38, 0.95);
"
>
<g clip-path="url(#_clipPath_5kVoellZ93LI5Lc2i2b27JZsraaBm0XM)">
<path
id="wave"
d=" M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z "
/>
</g>
</svg>
</div>
<div class="container-5">
<div class="row align-items-end">
<div class="col-lg-4 mb-4">
<p>
<img
:src="invertLogoUrl"
:alt="companyinfo?.company"
class="logo mb-1"
>
</p>
<p>{{ companyinfo?.contact }}</p>
<p>{{ companyinfo?.street }}</p>
<p>
{{ companyinfo?.postalcode }}
{{ companyinfo?.city }}
</p>
<p>{{ companyinfo?.district }}</p>
<br >
<p v-if="false" class="mb-4">
<span class="icon">
<svg>
<use xlink:href="/assets/icons/collection.svg#location"/>
</svg>
</span>{{ companyinfo?.latitude }} | {{ companyinfo?.longitude }}
</p>
<p>
<span class="icon">
<svg>
<use xlink:href="/assets/icons/collection.svg#phone"/>
</svg>
</span>{{ companyinfo?.phone }}
</p>
<p>
<span class="icon">
<svg>
<use xlink:href="/assets/icons/collection.svg#envelope"/>
</svg>
</span>{{ companyinfo?.email }}
</p>
<p>
<span class="icon">
<svg>
<use xlink:href="/assets/icons/collection.svg#desktop"/>
</svg>
</span>www.{{ companyinfo?.web }}
</p>
</div>
<div class="col-lg-3 pt-4 mb-4">
<div class="text-left footerNav">
<h3>{{ $t('importantLinks') }}</h3>
<p>
<NuxtLinkLocale
v-for="(key, index) in footerRouteNames"
:key="index"
:to="{ name: key }"
>
{{ $t(key) }}
</NuxtLinkLocale>
<NuxtLink to="/#faq">{{ $t('faq') }}</NuxtLink>
</p>
</div>
</div>
<div class="col-lg-5 mb-4">
<div class="certificates">
<img
:src="cmsUrl + '/uploads/exali_Siegel_5adfae16cb.jpg'"
alt="exali-Versicherungssiegel"
>
<img
:src="cmsUrl + '/uploads/XDI_zertifikat_162b61f4ad.png'"
alt="XDI-Zertifizierung"
>
</div>
<p class="mb-3">
Handcrafted webdesign with passion and
<span class="bigIcon heart">
<svg>
<use xlink:href="/assets/icons/collection.svg#heart"/>
</svg>
</span>
</p>
<p class="powered">
Powered by
<img
:src="cmsUrl + '/uploads/nuxt_Logo_white_1ad151de78.svg'"
alt="vue logo"
>
<span class="bigIcon">
<svg>
<use xlink:href="/assets/icons/collection.svg#plus"/>
</svg>
</span>
<img
:src="cmsUrl + '/uploads/strapi_logo_071ec5df4d.png'"
alt="strapi logo"
>
</p>
<p class="mt-4">
&copy; 2018-{{ currentYear }} by {{ companyinfo?.web }}
</p>
</div>
</div>
</div>
</footer>
</template>
<script setup>
const runtimeConfig = useRuntimeConfig();
const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl)
const mainStore = useMainStore()
const { companyinfo, invertLogoUrl } = storeToRefs(mainStore)
const currentYear = computed(() => new Date().getFullYear())
const footerRouteNames = ['imprint', 'privacy', 'magazin', 'terms']
</script>
<style lang="sass">
footer
background: rgba(38,38,38,.95)
position: relative
width: 100vw
color: white
z-index: 10
height: auto
min-height: 120px
margin-top: 100px
padding-bottom: 2rem
.topshape
fill: white
transform: translateY(-1px)
p
font-size: calc($fontSizeNormal - 20%)
line-height: 1.3rem
margin-bottom: 0.2rem
margin-top: .2rem
a
cursor: pointer
color: white
font-weight: bold
border-bottom: 0
&:hover
box-shadow: 0 0 20px 0 rgba($primaryColor, .3)
background-color: rgba($primaryColor, .2)
border-radius: 4px
.logo
width: 10rem !important
.icon
margin-right: 1rem
width: 1.2rem
display: inline-block
text-align: center
svg
width: 100%
fill: white
max-height: 1.5rem
.bigIcon
margin-right: .2rem
margin-left: .2rem
width: 1.2rem
display: inline-block
text-align: center
svg
width: 100%
fill: white
max-height: 1.5rem
&.heart
svg
fill: $pink
.certificates
img
height: 10vw
max-height: 80px
filter: grayscale(100%)
margin: 1rem
@media(max-width: $breakPointMD)
height: 18vw
margin-bottom: 2.5rem
.powered
img
height: 20px
margin: auto 15px
.footerNav
h3
color: #888
font-size: .8rem
text-transform: uppercase
font-weight: 800
margin-bottom: 0rem
p
margin-top: .5rem
display: inline-block
a
display: block
margin: .25rem 0
text-decoration: none
&.router-link-active
color: $pink
</style>

View File

@ -0,0 +1,319 @@
<template>
<header
:class="[{ mobile: screenWidth < 1350, desk: screenWidth >= 1350 }, { active: scrollPosition > 50 }]"
role="banner"
>
<SettingsPanel v-if="screenWidth >= 1350" />
<div class="headContent">
<div class="logoBox" role="button" tabindex="0" aria-label="Startseite" @click="navigateTo('/')">
<img :src="cmsUrl + '/uploads/DML_Logo_grey_2024_c51210b70c.svg'" alt="digimedialoop Logo" >
</div>
<div
class="navigationBox"
:class="[
isMenuOpen ? 'menu-active' : '',
screenWidth < 1350 ? 'mobile' : 'desk'
]"
role="navigation"
aria-label="Hauptnavigation"
tabindex="0"
>
<div class="closer"
@click="toggleMenu"
@keydown.enter="toggleMenu">
</div>
<nav v-if="isMenuOpen || screenWidth > 1350" aria-expanded="true">
<NuxtLinkLocale
v-for="link in navigationLinks"
:key="link.routeKey"
:to="localePath(link.routeKey)"
@click="handleMobileClose"
>
{{ $t(link.label) }}
</NuxtLinkLocale>
<a
class="menu_link" href="#"
role="button"
aria-label="Kontaktformular öffnen"
@click="toggleContactBubble"
>
{{ $t('contact') }}
</a>
<SettingsPanel v-if="screenWidth < 1350" />
</nav>
</div>
</div>
</header>
</template>
<script setup>
import { computed } from 'vue'
import { useMainStore } from '@/stores/main'
import { useRuntimeConfig, navigateTo } from '#app'
import { useLocalePath } from '#i18n'
const localePath = useLocalePath()
const mainStore = useMainStore()
const config = useRuntimeConfig()
const navigationLinks = [
{ routeKey: 'webagency', label: 'webagency' },
{ routeKey: 'services', label: 'services' },
{ routeKey: 'references', label: 'references' }
]
const isMenuOpen = computed(() => mainStore.menuOpen)
const scrollPosition = computed(() => mainStore.scrollPosition)
const screenWidth = computed(() => mainStore.screenWidth)
const cmsUrl = computed(() => config.public.cmsBaseUrl)
const toggleMenu = () => mainStore.toggleMenu()
const toggleContactBubble = () => mainStore.toggleContactBubble()
const handleMobileClose = () => {
if (screenWidth.value < 1350 && isMenuOpen.value) {
toggleMenu()
}
}
</script>
<style lang="sass">
header
position: fixed
top: 0
left: 0
margin: 0
width: 100%
background-image: linear-gradient(to bottom, rgba(white, 1), rgba(white, 1), rgba(white, 1), rgba(white, 0))
box-sizing: border-box
z-index: 20
&::before, &::after
content: ''
position: absolute
z-index: 90
backdrop-filter: blur(10px) brightness(1.05) // Glaseffekt: Unschärfe + leicht erhöhte Helligkeit
-webkit-backdrop-filter: blur(10px) brightness(1.05)
border: 1px solid rgba(255, 255, 255, 0.2) // leichte Transparenz des Randes
background: radial-gradient(circle, rgba(103, 202, 172, 0.8), rgba(103, 202, 172, 0.6), rgba(103, 202, 172, 0.4))
background-size: 150% 150%
opacity: 0.85 // Sichtbarkeit anpassen, aber noch leicht transparent
z-index: 6
transition: .8s
box-shadow: $innerShadow
overflow: hidden
&::before
width: 60vw
height: 18rem
border-radius: $loopShape
top: -12rem
right: -5vw
animation: bubble-wobble 8s infinite ease alternate, gradient-animation 20s infinite alternate ease-in-out
z-index: 6
@media(max-width: $breakPointMD)
height: 15rem
&::after
width: 28vw
height: 12rem
border-radius: $loopShape
top: -2rem
right: -6vw
animation: bubble-wobble 7s infinite ease alternate, gradient-animation 12s infinite alternate ease-in-out
@media(max-width: $breakPointMD)
height: 8rem
right: -10vw
// MOBILE NAVIGATION
&.mobile
top: 0
.headContent
padding: 0
.logoBox
width: 50%
z-index: 102
img
margin-top: 1.5rem //5rem
&.active
.logoBox
img
margin-top: 3rem
.navigationBox
display: block
position: relative
background-color: $darkgrey
width: 4rem
height: 4rem
z-index: 8
border-radius: 50%
margin-right: 5vw
margin-top: 2rem
.closer
position: relative
width: 100%
height: 4rem
&::after, &::before
position: absolute
content: ''
width: 2rem
z-index: 12
height: 5px
border-radius: 4px
background-color: white
right: 75%
transform: translateX(100%)
transition: .8s
&::before
top: 35%
&::after
top: 55%
nav
display: none
background-image: none
background: transparent
border: none
padding-top: 0rem !important
.menu_link
margin-left: 1.5rem
transition: .8s
&:hover
transform: scale(1.06)
background-image: radial-gradient(rgba($primaryColor, .1), transparent, transparent)
box-shadow: 0 0 0 0 transparent
border-radius: 20px
a, .menu_link
display: block
color: white
text-align: left
margin-bottom: .5rem
padding: 1.6rem 2.8rem .4rem
position: relative
font-size: 1.6rem !important
width: auto
max-width: 18rem
text-transform: uppercase
font-family: 'Mainfont-Bold'
&::before
content: ''
width: 1rem
height: .6rem
background-color: rgba($primaryColor, .9)
border-radius: $loopShape
position: absolute
top: 2.4rem
left: 1rem
border-radius: 20px
&:hover
transform: scale(1.06)
background-image: radial-gradient(rgba($primaryColor, .1), transparent, transparent)
box-shadow: 0 0 0 0 transparent
border-radius: 20px
&.menu-active
width: 100vw
height: 80vh
border-radius: 5px
margin: 0
background-color: rgba($darkgrey, .9)
.closer
&::before, &::after
top: 2rem
right: 1rem
&::before
transform: rotate(45deg)
&::after
transform: rotate(-45deg)
nav
display: block
padding: 10vh 0
margin: 0 5vw
.headContent
display: flex
align-items: top
justify-content: space-between
width: 100%
padding: 0 2rem
box-sizing: border-box
transition: .8s
z-index: 7
margin: 0
height: 0 //4rem
.logoBox
display: flex
align-self: center
justify-content: left
width: 33%
transition: .8s
background-color: transparent
margin-top: 4.5rem
@media(max-width: $breakPointMD)
margin-top: 3rem
img
width: 80%
max-width: 250px
margin: 4rem 5vw 0 5vw
transition: .8s
.navigationBox
position: relative
display: flex
align-items: center
justify-content: flex-end
width: 80%
transition: .8s
margin-top: 1.2rem //-1rem
nav
display: block
z-index: 102
//background: linear-gradient(to right, rgba($lightgrey, 0.8), rgba(white, 0.9), rgba(white, 0.9))
background: white
border: 1px solid adjust-color($beige, $lightness: 5%)
padding: 1rem 2rem
text-align: center
border-radius: 1rem
margin: 4.8rem 1rem 0 1rem
transition: .8s
a
margin: 0 1.2rem
text-decoration: none
color: $darkgrey
text-transform: uppercase
font-family: 'Comfortaa-Bold'
font-size: 1.1rem
letter-spacing: .05rem
transition: .6s
display: inline-block
&:hover
transform: scale(1.15)
background-image: radial-gradient(rgba(white, .5), rgba(white, .1))
box-shadow: 0 0 10px 10px rgba(white, 0.2)
border-radius: 10px
&.active
&::before
top: -13.5rem
&::after
top: -5rem
.headContent
padding: 0 //0 0 2.5rem 0
.navigationBox
margin-top: .5rem
nav
display: flex
margin: 2.5rem 0 0 0
padding: 1rem .5rem
border-top-right-radius: 0
border-top-left-radius: 0
border-bottom-left-radius: 50px
border-bottom-right-radius: 0
background: transparent
border: 1px solid transparent
a
font-size: 1rem
font-weight: bold
margin: 0 .8rem
.logoBox
align-items: left
img
margin-bottom: .5rem
width: 70%
max-width: 200px
</style>

View File

@ -0,0 +1,122 @@
// composables/useHtmlConverter.ts
interface TextChild {
type?: "text";
text: string;
bold?: boolean;
italic?: boolean;
underline?: boolean;
}
interface LinkChild {
type: "link";
url: string;
children: TextChild[];
}
type ParagraphChild = TextChild | LinkChild;
interface ParagraphBlock {
type: "paragraph";
children: ParagraphChild[];
}
interface HeadingBlock {
type: "heading";
level: number;
children: TextChild[];
}
interface ListBlock {
type: "list";
format: "unordered" | "ordered";
children: {
children: TextChild[];
}[];
}
type RichTextBlock = ParagraphBlock | HeadingBlock | ListBlock;
export function useHtmlConverter() {
const convertToHTML = (data: RichTextBlock[], prepend?: string): string => {
let html = "";
let firstParagraph = true;
if (Array.isArray(data)) {
data.forEach((item) => {
switch (item.type) {
case "heading":
if (item.children?.[0]?.text) {
html += `<h${item.level} role="heading" aria-level="${item.level}">`;
item.children.forEach((c) => {
if (c.bold) html += `<b>${c.text}</b>`;
else if (c.underline) html += `<u>${c.text}</u>`;
else if (c.italic) html += `<i>${c.text}</i>`;
else html += `${c.text}`;
});
html += `</h${item.level}>`;
}
break;
case "paragraph":
if (item.children?.[0]?.text) {
html += `<p>`;
if (firstParagraph && prepend !== undefined) {
html += `<b>${prepend}</b>`;
firstParagraph = false;
}
item.children.forEach((c) => {
if (c.type === "text") {
if (c.bold) html += `<b>${c.text}</b>`;
else if (c.underline) html += `<u>${c.text}</u>`;
else if (c.italic) html += `<i>${c.text}</i>`;
else html += `${c.text}`;
} else if (c.type === "link") {
html += `<a href="${c.url}">${c.children[0].text}</a>`;
}
});
html += `</p>`;
}
break;
case "list":
if (Array.isArray(item.children)) {
const tag = item.format === "ordered" ? "ol" : "ul";
html += `<${tag}>`;
item.children.forEach((listItem) => {
if (listItem.children?.[0]?.text) {
html += `<li><span>`;
listItem.children.forEach((c) => {
if (c.bold) html += `<b>${c.text}</b>`;
else if (c.underline) html += `<u>${c.text}</u>`;
else if (c.italic) html += `<i>${c.text}</i>`;
else html += `${c.text}`;
});
html += `</span></li>`;
}
});
html += `</${tag}>`;
}
break;
}
});
}
return html;
};
const convertToText = (data: RichTextBlock[]): string => {
let text = "";
if (Array.isArray(data)) {
data.forEach((item) => {
item.children?.forEach((child: any) => {
if (child.text) text += child.text + " ";
});
});
}
return text.trim();
};
return { convertToHTML, convertToText };
}

View File

@ -0,0 +1,43 @@
// composables/usePageMeta.ts
import { useRoute } from 'vue-router'
import { useMainStore } from '~/stores/main'
import { useHead } from '@unhead/vue'
export function usePageMeta() {
const route = useRoute()
const pageLink = route.path
const mainStore = useMainStore()
const page = mainStore.getPageByLink(pageLink)
console.log(page)
if (!page) {
console.warn(`Keine Seite gefunden für den pageLink "${pageLink}"`)
return
}
const metaTitle = page.SEO?.pageTitle || 'Standard Title'
const metaDescription = page.SEO?.seoDescription || 'Standard Description'
const metaImage = page.SEO?.seoImage?.url || '/default-image.jpg'
try {
JSON.stringify(metaTitle)
JSON.stringify(metaDescription)
JSON.stringify(metaImage)
} catch (err) {
console.error('Fehler beim Serialisieren der Meta-Daten:', err)
}
useHead({
title: metaTitle,
meta: [
{ name: 'description', content: metaDescription },
{ property: 'og:title', content: metaTitle },
{ property: 'og:description', content: metaDescription },
{ property: 'og:image', content: metaImage },
{ name: 'twitter:title', content: metaTitle },
{ name: 'twitter:description', content: metaDescription },
{ name: 'twitter:image', content: metaImage },
]
})
}

6
eslint.config.mjs Normal file
View File

@ -0,0 +1,6 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
// Your custom configs here
)

102
i18n/locales/de.json Normal file
View File

@ -0,0 +1,102 @@
{
"welcome": "Willkommen",
"webagency": "Webagentur",
"services": "Leistungen",
"contact": "Kontakt",
"references": "Referenzen",
"imprint": "Impressum",
"privacy": "Datenschutz",
"privacyPolicy": "Datenschutzerklärung",
"termsOfService": "Allgemeine Geschäftsbedingungen",
"terms": "AGB",
"faq": "Häufige Fragen",
"magazin": "Wissenswertes",
"accessability": "Barrierefreiheit",
"accessibilitySettings": "Einstellung der Barrierefreiheit",
"changeFontSize": "Text vergrößern",
"greyscale": "Graustufen",
"increaseContrast": "Kontrast erhöhen",
"borderFocus": "Fokus aktivieren",
"hideImages": "Bilder ausblenden",
"showLinks": "Links hervorheben",
"infoAccessibility": "Informationen zur Barrierefreiheit auf unserer Seite",
"importantLinks": "Wichtige Links",
"contactForm": {
"yourcontact2us": "Ihr Kontakt zu uns!",
"ourOffice": "Unsere Büroadresse",
"yourcontactperson": "Ihr Kontakt",
"name": "Name",
"email": "E-Mail",
"phone": "Telefon",
"message": "Nachricht",
"company": "Firma",
"sendMessage": "Nachricht senden",
"privacyInfotextBeforeLink": "Mit Absenden des Formulars stimmen Sie der Speicherung Ihrer Daten zwecks Kontaktaufnahme auf unserem Server zu.",
"privacyInfotextLinkText": "Informationen zum Datenschutz",
"validation": {
"nameRequired": "Name ist ein Pflichtfeld.",
"emailOrPhoneRequired": "Bitte geben Sie entweder eine E-Mail-Adresse oder eine Telefonnummer ein.",
"invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
"invalidPhone": "Bitte geben Sie eine gültige Telefonnummer ein."
},
"successMessage": "Ihre Nachricht wurde erfolgreich versendet.",
"errorMessage": "Leider gibt es momentan einen Fehler bei der Internetverbindung!",
"confirmation": {
"thx": "Vielen Dank für Ihre Nachricht!",
"info": "Wir werden uns umgehend bei Ihnen melden...",
"salutation": "Ihr digimedialoop Team"
}
},
"home": {
"heroBox": {
"h1": "Ihre Agentur für individuelles Webdesign und professionelle Webentwicklung",
"h2": "Modulare Webseiten mit modernsten Technologien",
"h3": "So ist Ihre Website schnell, effizient und zukunftssicher!"
},
"solution": {
"title": "Websites, die mehr können: Performance, Freiheit & KI-Power für Ihr Business",
"teaser": "Wir entwickeln maßgeschneiderte Webseiten mit JAMstack-Technologie, die perfekt auf Ihr Business abgestimmt sind und als leistungsstarkes Marketing- und Vertriebsinstrument für Ihren Erfolg sorgen.",
"text": "Durch die klare Trennung von Inhalt und Technik, unter Verwendung eines headless Content-Management-Systems, entstehen wartungsfreundliche, suchmaschinenoptimierte Lösungen, die nicht nur langfristig skalierbar sind, sondern auch Ihrem Marketing-Team die Arbeit erleichtern. Inhalte lassen sich ohne technische Hürden pflegen, neue Funktionen flexibel integrieren ganz ohne Plugin-Chaos oder Eingriffe ins Live-System. Dank sauberer semantischer Struktur sind unsere Lösungen zudem optimal auf AI-gestützte Suchsysteme vorbereitet und ermöglichen die einfache Integration in KI-gestützte Operator-Workflows.",
"buttonText": "Erfahren Sie mehr über Headless CMS"
},
"invitation": {
"title": "Ist Ihre Webseite bereit für die Zukunft?",
"teaser": "Wir zeigen Ihnen, wie Sie Ihre digitale Präsenz optimieren, Ihre Zielgruppe effektiv erreichen und langfristig von unseren skalierbaren, wartungsfreundlichen Lösungen profitieren können. Während einer kostenlosen Erstberatung erfahren Sie genau, welche Schritte notwendig sind, um Ihre Webseite in ein leistungsstarkes Marketing-Tool zu verwandeln.",
"button": "Kostenlose Erstberatung anfordern!"
},
"canDo": {
"title": "Nutzen auch Sie künftig das volle Potenzial Ihrer Webseite!",
"item1": {
"title": "Neukunden gewinnen und Umsatz steigern",
"text": "Machen Sie aus Besuchern zahlende Kunden! Mit einer klaren Strategie, überzeugendem Design und optimierter Nutzerführung wird Ihre Website zur Lead-Maschine."
},
"item2": {
"title": "Kunden und Mitglieder binden",
"text": "Stärken Sie die Beziehung zu Ihren Kunden! Mit wertvollen Inhalten, exklusiven Angeboten und interaktiven Funktionen bleibt Ihre Zielgruppe aktiv und engagiert."
},
"item3": {
"title": "Mitarbeiter finden und begeistern",
"text": "Gewinnen Sie die richtigen Talente! Eine authentische Karriereseite mit klaren Benefits macht Ihr Unternehmen für Bewerber unwiderstehlich."
},
"item4": {
"title": "Verwaltungs-Aufwand reduzieren",
"text": "Weniger Rückfragen mehr Effizienz! Durch klare Informationen und digitale Prozesse auf Ihrer Website sparen Sie Zeit, Kosten und entlasten Ihr Team."
}
},
"compBox": {
"title": "\"Design ist die Kunst, Funktion und Ästhetik zu vereinen\"",
"subtitle": "Mit diesem Anspruch starten wir in den Relaunch-Prozess unserer Kunden.",
"text": "Wir legen besonderen Wert auf ein aufgeräumtes Design, das den mentalen Modellen der Nutzer entspricht so finden die Besucher immer genau das, was sie suchen, an der Stelle, wo sie es erwarten."
},
"finalCall": {
"title": "Gemeinsam bringen wir Ihr Business auf das nächste Level!",
"button": "Kontaktieren Sie uns!"
},
"marqueeBanner": {
"title": "Diese Unternehmen vertrauen uns"
},
"faqArea": {
"headline": "Hier finden Sie Antworten auf häufig gestellte Fragen (FAQs) rund ums Thema Website-Erstellung mit digimedialoop"
}
}
}

102
i18n/locales/en.json Normal file
View File

@ -0,0 +1,102 @@
{
"welcome": "Welcome",
"webagency": "Web Agency",
"services": "Services",
"contact": "Contact",
"references": "References",
"imprint": "Imprint",
"privacy": "Privacy",
"privacyPolicy": "Privacy Policy",
"termsOfService": "Terms of Service",
"terms": "Terms",
"faq": "FAQ",
"magazin": "Insights",
"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",
"importantLinks": "Important Links",
"contactForm": {
"yourcontact2us": "Get in touch with us!",
"ourOffice": "Our Office Address",
"yourcontactperson": "Your Contact Person",
"name": "Name",
"email": "Email",
"phone": "Phone",
"message": "Message",
"company": "Company",
"sendMessage": "Send Message",
"privacyInfotextBeforeLink": "By submitting the form, you agree to the storage of your data on our server for the purpose of contacting you.",
"privacyInfotextLinkText": "Privacy Policy",
"validation": {
"nameRequired": "Name is a required field.",
"emailOrPhoneRequired": "Please enter either an email address or a phone number.",
"invalidEmail": "Please enter a valid email address.",
"invalidPhone": "Please enter a valid phone number."
},
"successMessage": "Your message has been sent successfully.",
"errorMessage": "There is currently a problem with the internet connection!",
"confirmation": {
"thx": "Thank you for your message!",
"info": "We will get back to you shortly...",
"salutation": "Your digimedialoop Team"
}
},
"home": {
"heroBox": {
"h1": "Your agency for custom web design and professional web development",
"h2": "Modular websites using the latest technologies",
"h3": "Making your website fast, efficient and future-proof!"
},
"solution": {
"title": "How your website becomes a real business tool",
"teaser": "We develop custom websites with JAMstack technology tailored to your business, serving as a powerful marketing and sales tool for your success.",
"text": "By clearly separating content and technology and using a headless content management system, we create low-maintenance, SEO-optimized solutions that are not only scalable long-term but also make work easier for your marketing team: content can be managed without technical barriers, new features integrated flexibly without plugin chaos or interfering with the live system.",
"buttonText": "Learn more about Headless CMS"
},
"invitation": {
"title": "Is your website ready for the future?",
"teaser": "We'll show you how to optimize your digital presence, effectively reach your target audience, and benefit long-term from our scalable, low-maintenance solutions. In a free initial consultation, youll learn exactly what steps are needed to turn your website into a powerful marketing tool.",
"button": "Request your free initial consultation!"
},
"canDo": {
"title": "Start using your websites full potential!",
"item1": {
"title": "Gain new customers and increase revenue",
"text": "Turn visitors into paying customers! With a clear strategy, compelling design and optimized user experience, your website becomes a lead machine."
},
"item2": {
"title": "Retain customers and members",
"text": "Strengthen your customer relationships! With valuable content, exclusive offers and interactive features, your target group remains active and engaged."
},
"item3": {
"title": "Attract and inspire new employees",
"text": "Find the right talent! An authentic career page with clear benefits makes your company irresistible to applicants."
},
"item4": {
"title": "Reduce administrative effort",
"text": "Fewer inquiries more efficiency! With clear information and digital processes on your website, you save time and costs while easing the workload for your team."
}
},
"compBox": {
"title": "\"Design is the art of uniting function and aesthetics\"",
"subtitle": "This is our approach when starting a clients relaunch process.",
"text": "We place great emphasis on a clean design that aligns with users mental models so visitors always find exactly what theyre looking for, right where they expect it."
},
"finalCall": {
"title": "Together, well take your business to the next level!",
"button": "Contact us!"
},
"marqueeBanner": {
"title": "These companies trust us"
},
"faqArea": {
"headline": "Here youll find answers to frequently asked questions (FAQs) about website creation with digimedialoop"
}
}
}

50
i18n/locales/es.json Normal file
View File

@ -0,0 +1,50 @@
{
"welcome": "Bienvenido",
"webagency": "Agencia Web",
"services": "Servicios",
"contact": "Contacto",
"references": "Referencias",
"imprint": "Aviso legal",
"privacy": "Privacidad",
"privacyPolicy": "Política de privacidad",
"termsOfService": "Términos del servicio",
"terms": "Condiciones",
"faq": "Preguntas frecuentes",
"magazin": "Conocimientos",
"accessability": "Accesibilidad",
"accessibilitySettings": "Configuración de accesibilidad",
"changeFontSize": "Aumentar tamaño del texto",
"greyscale": "Escala de grises",
"increaseContrast": "Aumentar contraste",
"borderFocus": "Activar enfoque",
"hideImages": "Ocultar imágenes",
"showLinks": "Resaltar enlaces",
"infoAccessibility": "Información sobre la accesibilidad de nuestro sitio",
"importantLinks": "Enlaces importantes",
"contactForm": {
"yourcontact2us": "¡Tu contacto con nosotros!",
"ourOffice": "Nuestra dirección",
"yourcontactperson": "Tu persona de contacto",
"name": "Nombre",
"email": "Correo electrónico",
"phone": "Teléfono",
"message": "Mensaje",
"company": "Empresa",
"sendMessage": "Enviar mensaje",
"privacyInfotextBeforeLink": "Al enviar el formulario, aceptas que tus datos se almacenen en nuestro servidor para contactarte.",
"privacyInfotextLinkText": "Política de privacidad",
"validation": {
"nameRequired": "El nombre es obligatorio.",
"emailOrPhoneRequired": "Introduce una dirección de correo electrónico o un número de teléfono.",
"invalidEmail": "Introduce una dirección de correo válida.",
"invalidPhone": "Introduce un número de teléfono válido."
},
"successMessage": "Tu mensaje ha sido enviado con éxito.",
"errorMessage": "¡Actualmente hay un problema con la conexión a Internet!",
"confirmation": {
"thx": "¡Gracias por tu mensaje!",
"info": "Nos pondremos en contacto contigo lo antes posible...",
"salutation": "Tu equipo de digimedialoop"
}
}
}

50
i18n/locales/fr.json Normal file
View File

@ -0,0 +1,50 @@
{
"welcome": "Bienvenue",
"webagency": "Agence Web",
"services": "Services",
"contact": "Contact",
"references": "Références",
"imprint": "Mentions légales",
"privacy": "Confidentialité",
"privacyPolicy": "Politique de confidentialité",
"termsOfService": "Conditions d'utilisation",
"terms": "CGU",
"faq": "FAQ",
"magazin": "Informations",
"accessability": "Accessibilité",
"accessibilitySettings": "Paramètres d'accessibilité",
"changeFontSize": "Augmenter la taille du texte",
"greyscale": "Niveaux de gris",
"increaseContrast": "Augmenter le contraste",
"borderFocus": "Activer le focus",
"hideImages": "Masquer les images",
"showLinks": "Mettre en évidence les liens",
"infoAccessibility": "Informations sur l'accessibilité de notre site",
"importantLinks": "Liens importants",
"contactForm": {
"yourcontact2us": "Votre contact avec nous !",
"ourOffice": "Notre adresse",
"yourcontactperson": "Votre interlocuteur",
"name": "Nom",
"email": "Email",
"phone": "Téléphone",
"message": "Message",
"company": "Entreprise",
"sendMessage": "Envoyer le message",
"privacyInfotextBeforeLink": "En soumettant le formulaire, vous acceptez que vos données soient stockées sur notre serveur pour vous contacter.",
"privacyInfotextLinkText": "Politique de confidentialité",
"validation": {
"nameRequired": "Le nom est obligatoire.",
"emailOrPhoneRequired": "Veuillez saisir une adresse email ou un numéro de téléphone.",
"invalidEmail": "Veuillez saisir une adresse email valide.",
"invalidPhone": "Veuillez saisir un numéro de téléphone valide."
},
"successMessage": "Votre message a été envoyé avec succès.",
"errorMessage": "Il y a actuellement un problème de connexion Internet !",
"confirmation": {
"thx": "Merci pour votre message !",
"info": "Nous vous contacterons rapidement...",
"salutation": "Votre équipe digimedialoop"
}
}
}

50
i18n/locales/it.json Normal file
View File

@ -0,0 +1,50 @@
{
"welcome": "Benvenuto",
"webagency": "Agenzia Web",
"services": "Servizi",
"contact": "Contatto",
"references": "Referenze",
"imprint": "Impressum",
"privacy": "Privacy",
"privacyPolicy": "Informativa sulla privacy",
"termsOfService": "Termini di servizio",
"terms": "Condizioni",
"faq": "Domande frequenti",
"magazin": "Approfondimenti",
"accessability": "Accessibilità",
"accessibilitySettings": "Impostazioni di accessibilità",
"changeFontSize": "Aumenta la dimensione del testo",
"greyscale": "Scala di grigi",
"increaseContrast": "Aumenta il contrasto",
"borderFocus": "Attiva contorno di messa a fuoco",
"hideImages": "Nascondi immagini",
"showLinks": "Evidenzia i link",
"infoAccessibility": "Informazioni sull'accessibilità del nostro sito",
"importantLinks": "Link importanti",
"contactForm": {
"yourcontact2us": "Il tuo contatto con noi!",
"ourOffice": "Il nostro indirizzo",
"yourcontactperson": "Il tuo referente",
"name": "Nome",
"email": "Email",
"phone": "Telefono",
"message": "Messaggio",
"company": "Azienda",
"sendMessage": "Invia messaggio",
"privacyInfotextBeforeLink": "Inviando il modulo, acconsenti alla memorizzazione dei tuoi dati sul nostro server allo scopo di essere contattato.",
"privacyInfotextLinkText": "Informativa sulla privacy",
"validation": {
"nameRequired": "Il nome è obbligatorio.",
"emailOrPhoneRequired": "Inserisci un indirizzo email o un numero di telefono.",
"invalidEmail": "Inserisci un indirizzo email valido.",
"invalidPhone": "Inserisci un numero di telefono valido."
},
"successMessage": "Il tuo messaggio è stato inviato con successo.",
"errorMessage": "Al momento si è verificato un errore nella connessione a Internet!",
"confirmation": {
"thx": "Grazie per il tuo messaggio!",
"info": "Ti contatteremo il prima possibile...",
"salutation": "Il tuo team digimedialoop"
}
}
}

50
i18n/locales/tr.json Normal file
View File

@ -0,0 +1,50 @@
{
"welcome": "Hoş geldiniz",
"webagency": "Web Ajansı",
"services": "Hizmetler",
"contact": "İletişim",
"references": "Referanslar",
"imprint": "Künye",
"privacy": "Gizlilik",
"privacyPolicy": "Gizlilik Politikası",
"termsOfService": "Hizmet Şartları",
"terms": "Şartlar",
"faq": "SSS",
"magazin": "Bilgilendirme",
"accessability": "Erişilebilirlik",
"accessibilitySettings": "Erişilebilirlik Ayarları",
"changeFontSize": "Yazı boyutunu büyüt",
"greyscale": "Gri tonlar",
"increaseContrast": "Kontrastı artır",
"borderFocus": "Odak kenarlığını etkinleştir",
"hideImages": "Resimleri gizle",
"showLinks": "Bağlantıları vurgula",
"infoAccessibility": "Sitemizin erişilebilirliği hakkında bilgiler",
"importantLinks": "Önemli Bağlantılar",
"contactForm": {
"yourcontact2us": "Bizimle iletişiminiz!",
"ourOffice": "Ofis adresimiz",
"yourcontactperson": "İlgili kişi",
"name": "Ad",
"email": "E-posta",
"phone": "Telefon",
"message": "Mesaj",
"company": "Firma",
"sendMessage": "Mesaj gönder",
"privacyInfotextBeforeLink": "Formu göndererek, iletişim amacıyla verilerinizin sunucumuzda saklanmasına izin vermiş olursunuz.",
"privacyInfotextLinkText": "Gizlilik Politikası",
"validation": {
"nameRequired": "Ad alanı zorunludur.",
"emailOrPhoneRequired": "Lütfen e-posta adresi veya telefon numarası girin.",
"invalidEmail": "Lütfen geçerli bir e-posta adresi girin.",
"invalidPhone": "Lütfen geçerli bir telefon numarası girin."
},
"successMessage": "Mesajınız başarıyla gönderildi.",
"errorMessage": "Şu anda internet bağlantısında bir sorun var!",
"confirmation": {
"thx": "Mesajınız için teşekkürler!",
"info": "En kısa sürede sizinle iletişime geçeceğiz...",
"salutation": "digimedialoop Ekibiniz"
}
}
}

191
layouts/default.vue Normal file
View File

@ -0,0 +1,191 @@
<!-- layouts/default.vue -->
<template>
<div>
<PageHeader />
<ContactForm />
<main>
<Breadcrumbs />
<slot />
</main>
<PageFooter />
</div>
</template>
<script setup>
import { usePageMeta } from '~/composables/usePageMeta'
usePageMeta()
</script>
<style lang="sass">
main
margin-top: 0 //11rem
font-family: 'Mainfont'
min-height: 45vh
z-index: 3
h1
font-family: 'Comfortaa'
font-size: $fontSizeLarge //calc(1.2rem + 1.2vw)
margin-top: calc(.6rem + .6vw)
margin-bottom: calc(1.2rem + 1.2vw)
line-height: 150%
font-weight: normal
@media(max-width: $breakPointSM)
font-size: 1.5rem
line-height: 2rem
h2
font-family: 'Comfortaa'
font-size: $fontSizeMedium //calc(.9rem + 1vw)
margin-top: calc(.6rem + .6vw)
margin-bottom: calc(1.2rem + 1.2vw)
line-height: 150%
font-weight: normal
h3
font-family: 'Comfortaa'
font-size: calc(#{$fontSizeMedium} * 0.7) //calc(.9rem + 1vw)
margin-top: calc(.2rem + .6vw)
margin-bottom: calc(1.2rem + 1.2vw)
line-height: 150%
font-weight: normal
p
font-size: 1.1rem
line-height: 150%
b, bold, strong
font-family: 'Mainfont-Bold'
u
text-decoration: none
position: relative
&::before
content: ""
transform: rotate(-2deg)
border-bottom: 3px solid rgba(103,202,172,.25)
position: absolute
bottom: 4px
left: 0
width: 100%
box-shadow: 4px 4px 2px 1px rgba(103,202,172,.25)
a
color: darken($primaryColor, 20%)
.supheadlinePink, supheadlineMint
margin-bottom: -.5rem
font-size: calc(.5rem + 1vw)
.supheadlinePink
color: darken($pink, 10%)
.supheadlineMint
color: darken($primaryColor, 5%)
.imgRight, .imgLeft
width: 45%
@media(max-width: $breakPointLG)
float: none
width: 80%
margin: 1rem 10%
.imgRight
float: right
margin: 2rem 0 2rem 2rem
@media(max-width: $breakPointLG)
float: none
max-width: 100%
.imgLeft
float: left
margin: 2rem 2rem 2rem 0
@media(max-width: $breakPointLG)
float: none
max-width: 100%
.loopShape
border-radius: $loopShape
button
background-color: white
border: 1px solid $darkgrey
border-radius: 5px
padding: 0.5rem 1rem
font-size: 1.2rem
text-transform: uppercase
position: relative
overflow: hidden
transition: all 0.4s ease-in-out
z-index: 1
color: $darkgrey
&::before
content: ''
position: absolute
top: 50%
left: 50%
width: 300%
height: 300%
background-color: rgba($primaryColor, 0.4)
transition: transform 0.4s ease-in-out
border-radius: 50%
transform: translate(-50%, -50%) scale(0)
z-index: 1
span, a
position: relative
z-index: 2
&:hover
box-shadow: 0 0 15px rgba($primaryColor, 0.2), 0 0 25px rgba($primaryColor, 0.2)
border: 1px solid $primaryColor
//letter-spacing: .05rem
transform: scale(1.02)
background-image: linear-gradient(to top left, $primaryColor, lighten($primaryColor, 30%))
/*&::before
transform: translate(-50%, -50%) scale(1) */
&.pinkBtn
background-color: $pink
color: white
border: 1px solid $pink
&:hover
background-image: linear-gradient(to top left, $pink, lighten($pink, 5%))
&.mintBtn
background-color: $primaryColor
color: white
border: 1px solid $primaryColor
&:hover
background-image: linear-gradient(to top left, $primaryColor, lighten($primaryColor, 5%))
&.readBtn
background-color: $primaryColor
color: white
font-size: .9rem
border: none
padding: .4rem .8rem
margin: 0 0 1rem 0
section
margin-bottom: 5vh
position: relative
&:first-of-type
&::before
content: ''
width: 12vw
height: 95%
min-height: 400px
max-height: 600px
background-color: rgba($primaryColor, .7)
border-radius: $loopShape
position: absolute
top: 10vh //3%
left: -8vw
z-index: 20
animation: bubble-wobble 5s infinite ease alternate
box-shadow: $innerShadow
transition: left 0.3s
&.beigeBG
background-color: $beige
min-height: 200px
.topSpace
padding-top: 9rem
.container
width: 80%
margin: auto 10%
&.mobile
margin-top: 0 //20vh
</style>

152
nuxt.config.ts Normal file
View File

@ -0,0 +1,152 @@
import { defineNuxtConfig } from 'nuxt/config'
export default defineNuxtConfig({
app: {
head: {
title: 'digimedialoop',
htmlAttrs: {
lang: 'de',
},
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
],
charset: 'utf-16',
viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
}
},
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
vite: {
css: {
preprocessorOptions: {
sass: {
additionalData: `
@use "~/assets/styles/bootstrap.sass" as *\n
@use "~/assets/styles/main.sass" as *\n
`
}
}
}
},
modules: [
'@nuxt/eslint',
'@nuxt/image',
'@nuxt/scripts',
'@nuxt/ui',
'@pinia/nuxt',
'@nuxtjs/i18n',
['@pinia/nuxt', {
autoImports: [
'defineStore',
'storeToRefs',
['defineStore', 'definePiniaStore']
]
}],
],
runtimeConfig: {
public: {
cmsBaseUrl: process.env.CMS_URL,
cmsToken: process.env.CMS_TOKEN
}
},
components: [
{ path: '~/components', pathPrefix: false },
{ path: '~/components/template', pathPrefix: false }
],
i18n: {
defaultLocale: 'de',
strategy: 'prefix_except_default',
locales: [
{ code: 'de', name: 'Deutsch', file: 'de.json' },
{ code: 'en', name: 'English', file: 'en.json' },
{ code: 'es', name: 'Español', file: 'es.json' },
{ code: 'fr', name: 'Français', file: 'fr.json' },
{ code: 'it', name: 'Italiano', file: 'it.json' },
{ code: 'tr', name: 'Türkçe', file: 'tr.json' }
],
customRoutes: 'config',
pages: {
index: {
de: '/',
en: '/home',
fr: '/accueil',
it: '/home',
es: '/inicio',
tr: '/anasayfa'
},
webagency: {
de: '/webagentur',
en: '/webagency',
fr: '/agence-web',
it: '/agenzia-web',
es: '/agencia-web',
tr: '/web-ajansi'
},
services: {
de: '/leistungen',
en: '/services',
fr: '/services',
it: '/servizi',
es: '/servicios',
tr: '/hizmetler'
},
references: {
de: '/referenzen',
en: '/references',
fr: '/références',
it: '/referenze',
es: '/referencias',
tr: '/referanslar'
},
imprint: {
de: '/impressum',
en: '/imprint',
fr: '/mentions-legales',
it: '/note-legali',
es: '/aviso-legal',
tr: '/künye'
},
privacy: {
de: '/datenschutz',
en: '/privacy',
fr: '/confidentialite',
it: '/privacy',
es: '/privacidad',
tr: '/gizlilik'
},
terms: {
de: '/agb',
en: '/terms',
fr: '/conditions',
it: '/termini',
es: '/condiciones',
tr: '/kosullar'
},
magazin: {
de: '/wissenswertes',
en: '/magazine',
fr: '/magazine',
it: '/magazine',
es: '/revista',
tr: '/dergi'
}
},
bundle: {
optimizeTranslationDirective: false
}
},
pinia: {
autoImports: [
'defineStore',
['defineStore', 'definePiniaStore'],
'storeToRefs'
]
},
nitro: {
prerender: {
crawlLinks: true,
failOnError: false,
//routes: ['/', '/webagency'] // Wichtige Routen vorrendern , '/impressum', '/datenschutz'
}
}
})

17108
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "npm run lint:eslint && npm run lint:prettier",
"lint:eslint": "eslint . --fix",
"lint:prettier": "prettier . --check",
"lintfix": "eslint . --fix && prettier --write --list-different ."
},
"dependencies": {
"@iconify-json/ri": "^1.2.5",
"@nuxt/eslint": "^1.3.0",
"@nuxt/icon": "^1.12.0",
"@nuxt/image": "^1.10.0",
"@nuxt/scripts": "^0.11.6",
"@nuxt/ui": "^3.0.2",
"@nuxtjs/i18n": "^9.5.3",
"@pinia/nuxt": "^0.11.0",
"nitropack": "^2.11.9",
"nuxt": "^3.16.2",
"typescript": "^5.8.3"
},
"devDependencies": {
"eslint": "^9.25.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-vue": "^10.0.0",
"prettier": "^3.5.3",
"sass": "^1.86.3",
"sass-loader": "^16.0.5"
}
}

15
pages/imprint/index.vue Normal file
View File

@ -0,0 +1,15 @@
<template>
<div class="container-10">
<h1>{{ $t('imprint') }}</h1>
</div>
</template>
<script setup>
definePageMeta({
layout: 'default'
})
</script>
<style lang="sass">
</style>

347
pages/index.vue Normal file
View File

@ -0,0 +1,347 @@
<template>
<div class="homePage">
<section class="heroBox">
<div class="container-10">
<h1>{{ $t('home.heroBox.h1') }}</h1>
<h2>{{ $t('home.heroBox.h2') }}</h2>
<h3>{{ $t('home.heroBox.h3') }}</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">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"></path>
</svg>
</section>
<section>
<div class="container-10 webStrategy">
<img class="imgFloatLeft" src="https://strapi.digimedialoop.de/uploads/web_wireframe_Strategie_0bae802a68.png" alt="wireframe web strategie">
<h2>{{ $t('home.solution.title') }}</h2>
<h3>{{ $t('home.solution.teaser') }}</h3>
<p>{{ $t('home.solution.text') }}</p>
<button class="mintBtn"
role="button"
aria-label="headless CMS Info" @click="navigateToArticle">{{ $t('home.solution.buttonText') }}</button>
</div>
</section>
<section class="targetGroup">
<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>
</svg>
<div class="container-10">
<div class="row">
<div class="col-md-4">
</div>
<div class="col-md-8 pt-5 pb-5">
<h2>{{ $t('home.invitation.title') }}</h2>
<h3>{{ $t('home.invitation.teaser') }}</h3>
<button class="pinkBtn" @click.prevent="toggleContactBubble"
role="button"
aria-label="Kontaktformular öffnen">{{ $t('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">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"></path>
</svg>
</section>
<section class="canDo">
<div class="container">
<h2 class="text-center">{{ $t('home.canDo.title') }}</h2>
<div class="row mb-5">
<div class="col-xl-6">
<div class="row">
<div class="col-md-6 my-5">
<div class="innerBox">
<div class="canDoItem">
<div class="imageBox" style="background-image: url('https://strapi.digimedialoop.de/uploads/website_Erfolg_Marketing_3c36a43ba5.png');"></div>
<h3>{{ $t('home.canDo.item1.title') }}</h3>
<p>{{ $t('home.canDo.item1.text') }}</p>
</div>
</div>
</div>
<div class="col-md-6 my-5">
<div class="innerBox">
<div class="canDoItem">
<div class="imageBox" style="background-image: url('https://strapi.digimedialoop.de/uploads/Kundenbindung_45d45ef3fc.png');"></div>
<h3>{{ $t('home.canDo.item2.title') }}</h3>
<p>{{ $t('home.canDo.item2.text') }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-6">
<div class="row">
<div class="col-md-6 my-5">
<div class="innerBox">
<div class="canDoItem">
<div class="imageBox" style="background-image: url('https://strapi.digimedialoop.de/uploads/Screen_Shot_Tool_20250228133408_beb2a11980.png');"></div>
<h3>{{ $t('home.canDo.item3.title') }}</h3>
<p>{{ $t('home.canDo.item3.text') }}</p>
</div>
</div>
</div>
<div class="col-md-6 my-5">
<div class="innerBox">
<div class="canDoItem">
<div class="imageBox" style="background-image: url('https://strapi.digimedialoop.de/uploads/Screen_Shot_Tool_20250228133812_0a20d4320e.png');"></div>
<h3>{{ $t('home.canDo.item4.title') }}</h3>
<p>{{ $t('home.canDo.item4.text') }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="compBox">
<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>
</svg>
<div class="container-10 pb-5">
<div class="row d-flex align-items-center">
<div class="col-md-6">
<h2>{{ $t('home.compBox.title') }}</h2>
<h3>{{ $t('home.compBox.subtitle') }}</h3>
<p>{{ $t('home.compBox.text') }}</p>
</div>
<div class="col-md-6 mt-5 pt-4 pb-4">
<ImageComparisonSlider
beforeImage="https://strapi.digimedialoop.de/uploads/Image_Comp_BSK_Before_1ae2a67e1b.png"
afterImage="https://strapi.digimedialoop.de/uploads/Image_Comp_BSK_After_b4358b0e19.png"
/>
</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">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"></path>
</svg>
</section>
<section>
<div class="container-10 text-center py-5">
<h3>{{ $t('home.finalCall.title') }}</h3>
<button class="pinkBtn mt-3"
@click.prevent="toggleContactBubble"
role="button"
:aria-label="$t('home.finalCall.button')">{{ $t('home.finalCall.button') }}</button>
</div>
</section>
<MarqueeBanner :items="customers" :logoHeight="60" :title="$t('home.marqueeBanner.title')" />
<FAQArea pageLink="/" :headline="$t('home.faqArea.headline')" />
</div>
</template>
<script setup>
import { useMainStore } from '@/stores/main';
import { storeToRefs } from 'pinia';
import { defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const mainStore = useMainStore();
const { cmsUrl, customers } = storeToRefs(mainStore);
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 waveHeight = computed(() => (screenWidth.value / 25).toFixed(0));
</script>
<style lang="sass">
.homePage
.heroBox
background-image: url('https://strapi.digimedialoop.de/uploads/DML_Home_hero_f0916b5608.png')
background-repeat: no-repeat
background-size: cover
background-position: right bottom
min-height: 35rem
height: 70vh
max-height: 200vw
display: flex
align-items: center
justify-content: center
position: relative
@media (max-width: $breakPointMD)
background-position: center bottom
h1
margin-top: 3rem
z-index: 2
color: mix(black, $pink, 2%)
font-size: clamp(1rem, .8rem + 1vw, 1.2rem)
line-height: 1.5
margin-bottom: 0
max-width: 70%
@media (max-width: $breakPointMD)
max-width: 100%
h2
z-index: 2
font-size: clamp(1.4rem, .8rem + 2vw, 2.4rem)
line-height: 1.5
margin-top: 1rem
max-width: 55%
@media (max-width: $breakPointMD)
max-width: 90%
h3
max-width: 55%
line-height: 1.5
font-size: clamp(1rem, .8rem + 1vw, 1.6rem)
@media (max-width: $breakPointMD)
max-width: 90%
&::after
content: ''
position: absolute
top: 0
left: 0
width: 50%
height: 100%
background-image: linear-gradient(to right, rgba(white, .3), transparent)
z-index: 1
.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
h2
font-size: clamp(1.6rem, 1rem + 1vw, 1.8rem)
line-height: 150%
h3
font-size: clamp(1.2rem, .8rem + 1vw, 1.4rem)
line-height: 150%
img
width: 80%
margin: 0 2rem 1rem 0
max-width: 300px
float: left
.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
h3
font-size: clamp(1.1rem, .8rem + 1vw, 1.2rem)
line-height: 150%
.firstTeaser
h2
font-size: 1.6rem
line-height: 2.4rem
.subLine
color: adjust-color($darkgrey, $lightness: 20%)
font-size: 80%
.pinkFont
color: darken($pink, 10%)
.imgRight
float: right
max-width: 50%
.homeImageTop
margin: 4.5rem 0 8vh 3rem !important
.compBox
background-image: linear-gradient(to bottom left, white, #FEDEE8, white)
padding: 5rem 0 3rem 0
h3
line-height: 1.5
p
padding-right: 1rem
.canDo
margin: 15vh 0
h2
margin-bottom: 3.5rem
.row
display: flex
flex-wrap: wrap
height: 100%
align-items: stretch
.innerBox
width: 90%
margin: 0 5% 0 5%
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: 0 2rem 1rem 2rem
border-right: 1px solid lighten($beige, 0%)
border-bottom: 1px solid lighten($beige, 0%)
height: 100%
.canDoItem
display: flex
flex-direction: column
align-items: center
justify-content: flex-start
text-align: center
color: black
height: 100%
margin-top: .5rem
&:nth-child(1) .imageBox
border-radius: $loopShape1
&:nth-child(2) .imageBox
border-radius: $loopShape2
&:nth-child(3) .imageBox
border-radius: $loopShape3
&:nth-child(4) .imageBox
border-radius: $loopShape4
.imageBox
width: 100%
max-width: 280px
padding-bottom: 75% /* 4:3 Verhältnis */
background-size: cover
background-position: center
margin-bottom: 2rem
h3
font-size: 1.2rem
line-height: 1.5rem
text-align: center
font-family: 'Mainfont-Bold'
color: darken($pink, 10%)
text-transform: uppercase
h4
font-size: 1.4rem
line-height: 1.8rem
margin: 2rem 0
font-family: 'Mainfont-Bold'
p
font-size: .9rem
text-align: left !important
</style>

15
pages/magazin/index.vue Normal file
View File

@ -0,0 +1,15 @@
<template>
<div class="container-10">
<h1>{{ $t('magazin') }}</h1>
</div>
</template>
<script setup>
definePageMeta({
layout: 'default'
})
</script>
<style lang="sass">
</style>

15
pages/privacy/index.vue Normal file
View File

@ -0,0 +1,15 @@
<template>
<div class="container-10">
<h1>{{ $t('privacy') }}</h1>
</div>
</template>
<script setup>
definePageMeta({
layout: 'default'
})
</script>
<style lang="sass">
</style>

View File

@ -0,0 +1,15 @@
<template>
<div class="container-10">
<h1>{{ $t('references') }}</h1>
</div>
</template>
<script setup>
definePageMeta({
layout: 'default'
})
</script>
<style lang="sass">
</style>

15
pages/services/index.vue Normal file
View File

@ -0,0 +1,15 @@
<template>
<div class="container-10">
<h1>{{ $t('services') }}</h1>
</div>
</template>
<script setup>
definePageMeta({
layout: 'default'
})
</script>
<style lang="sass">
</style>

15
pages/terms/index.vue Normal file
View File

@ -0,0 +1,15 @@
<template>
<div class="container-10">
<h1>{{ $t('termsOfService') }}</h1>
</div>
</template>
<script setup>
definePageMeta({
layout: 'default'
})
</script>
<style lang="sass">
</style>

18
pages/webagency/index.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<section>
<div class="container-10">
<h1>{{ $t('webagency') }}</h1>
</div>
</section>
</template>
<script setup>
definePageMeta({
layout: 'default'
})
</script>
<style lang="sass">
</style>

4
plugins/store.server.ts Normal file
View File

@ -0,0 +1,4 @@
export default defineNuxtPlugin(async (nuxtApp) => {
const store = useMainStore()
await store.fetchInitialData()
})

2
public/_robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-Agent: *
Disallow:

View File

@ -0,0 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="heart" viewBox="0 0 512 512">
<path d="M47.6 300.4L228.3 469.1c7.5 7 17.4 10.9 27.7 10.9s20.2-3.9 27.7-10.9L464.4 300.4c30.4-28.3 47.6-68 47.6-109.5v-5.8c0-69.9-50.5-129.5-119.4-141C347 36.5 300.6 51.4 268 84L256 96 244 84c-32.6-32.6-79-47.5-124.6-39.9C50.5 55.6 0 115.2 0 185.1v5.8c0 41.5 17.2 81.2 47.6 109.5z"/>
</symbol>
<symbol id="plus" viewBox="0 0 448 512">
<path d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"/>
</symbol>
<symbol id="location" viewBox="0 0 384 512">
<path d="M215.7 499.2C267 435 384 279.4 384 192C384 86 298 0 192 0S0 86 0 192c0 87.4 117 243 168.3 307.2c12.3 15.3 35.1 15.3 47.4 0zM192 128a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"/>
</symbol>
<symbol id="phone" viewBox="0 0 512 512">
<path d="M164.9 24.6c-7.7-18.6-28-28.5-47.4-23.2l-88 24C12.1 30.2 0 46 0 64C0 311.4 200.6 512 448 512c18 0 33.8-12.1 38.6-29.5l24-88c5.3-19.4-4.6-39.7-23.2-47.4l-96-40c-16.3-6.8-35.2-2.1-46.3 11.6L304.7 368C234.3 334.7 177.3 277.7 144 207.3L193.3 167c13.7-11.2 18.4-30 11.6-46.3l-40-96z"/>
</symbol>
<symbol id="envelope" viewBox="0 0 512 512">
<path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/>
</symbol>
<symbol id="desktop" viewBox="0 0 576 512">
<path d="M64 0C28.7 0 0 28.7 0 64v288c0 35.3 28.7 64 64 64h176l-10.7 32H160c-17.7 0-32 14.3-32 32s14.3 32 32 32h256c17.7 0 32-14.3 32-32s-14.3-32-32-32h-69.3L336 416h176c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H64zm448 64v224H64V64h448z"/>
</symbol>
<symbol id="talk" viewBox="0 0 640 512">
<path d="M208 352c114.9 0 208-78.8 208-176S322.9 0 208 0S0 78.8 0 176c0 38.6 14.7 74.3 39.6 103.4c-3.5 9.4-8.7 17.7-14.2 24.7c-4.8 6.2-9.7 11-13.3 14.3c-1.8 1.6-3.3 2.9-4.3 3.7c-.5 .4-.9 .7-1.1 .8l-.2 .2s0 0 0 0s0 0 0 0C1 327.2-1.4 334.4 .8 340.9S9.1 352 16 352c21.8 0 43.8-5.6 62.1-12.5c9.2-3.5 17.8-7.4 25.2-11.4C134.1 343.3 169.8 352 208 352zM448 176c0 112.3-99.1 196.9-216.5 207C255.8 457.4 336.4 512 432 512c38.2 0 73.9-8.7 104.7-23.9c7.5 4 16 7.9 25.2 11.4c18.3 6.9 40.3 12.5 62.1 12.5c6.9 0 13.1-4.5 15.2-11.1c2.1-6.6-.2-13.8-5.8-17.9c0 0 0 0 0 0s0 0 0 0l-.2-.2c-.2-.2-.6-.4-1.1-.8c-1-.8-2.5-2-4.3-3.7c-3.6-3.3-8.5-8.1-13.3-14.3c-5.5-7-10.7-15.4-14.2-24.7c24.9-29 39.6-64.7 39.6-103.4c0-92.8-84.9-168.9-192.6-175.5c.4 5.1 .6 10.3 .6 15.5z"/>
</symbol>
<symbol id="nav_right" viewBox="0 0 320 512">
<path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"/>
</symbol>
<symbol id="nav_left" viewBox="0 0 320 512">
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/>
</symbol>
<symbol id="times" viewBox="0 0 352 512">
<path d="M242.7 256l100.1-100.1c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L197.4 210.7 97.4 110.6c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l100.1 100.1L52.1 356.1c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l100.1-100.1 100.1 100.1c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L242.7 256z"/>
</symbol>
<symbol id="xing" viewBox="0 0 384 512">
<path d="M162.7 210c-1.8 3.3-25.2 44.4-70.1 123.5-4.9 8.3-10.8 12.5-17.7 12.5H9.8c-7.7 0-12.1-7.5-8.5-14.4l69-121.3c.2 0 .2-.1 0-.3l-43.9-75.6c-4.3-7.8 .3-14.1 8.5-14.1H100c7.3 0 13.3 4.1 18 12.2l44.7 77.5zM382.6 46.1l-144 253v.3L330.2 466c3.9 7.1 .2 14.1-8.5 14.1h-65.2c-7.6 0-13.6-4-18-12.2l-92.4-168.5c3.3-5.8 51.5-90.8 144.8-255.2 4.6-8.1 10.4-12.2 17.5-12.2h65.7c8 0 12.3 6.7 8.5 14.1z"/>
</symbol>
<symbol id="linkedin" viewBox="0 0 448 512">
<path d="M100.3 448H7.4V148.9h92.9zM53.8 108.1C24.1 108.1 0 83.5 0 53.8a53.8 53.8 0 0 1 107.6 0c0 29.7-24.1 54.3-53.8 54.3zM447.9 448h-92.7V302.4c0-34.7-.7-79.2-48.3-79.2-48.3 0-55.7 37.7-55.7 76.7V448h-92.8V148.9h89.1v40.8h1.3c12.4-23.5 42.7-48.3 87.9-48.3 94 0 111.3 61.9 111.3 142.3V448z"/>
</symbol>
<symbol id="accessibility" viewBox="0 0 512 512">
<path d="M0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm161.5-86.1c-12.2-5.2-26.3 .4-31.5 12.6s.4 26.3 12.6 31.5l11.9 5.1c17.3 7.4 35.2 12.9 53.6 16.3l0 50.1c0 4.3-.7 8.6-2.1 12.6l-28.7 86.1c-4.2 12.6 2.6 26.2 15.2 30.4s26.2-2.6 30.4-15.2l24.4-73.2c1.3-3.8 4.8-6.4 8.8-6.4s7.6 2.6 8.8 6.4l24.4 73.2c4.2 12.6 17.8 19.4 30.4 15.2s19.4-17.8 15.2-30.4l-28.7-86.1c-1.4-4.1-2.1-8.3-2.1-12.6l0-50.1c18.4-3.5 36.3-8.9 53.6-16.3l11.9-5.1c12.2-5.2 17.8-19.3 12.6-31.5s-19.3-17.8-31.5-12.6L338.7 175c-26.1 11.2-54.2 17-82.7 17s-56.5-5.8-82.7-17l-11.9-5.1zM256 160a40 40 0 1 0 0-80 40 40 0 1 0 0 80z"/>
</symbol>
<symbol viewBox="0 0 15 15" id="radix-icons-accessibility">
<path fill="currentColor" fill-rule="evenodd" d="M.877 7.5a6.623 6.623 0 1 1 13.246 0a6.623 6.623 0 0 1-13.246 0M7.5 1.827a5.673 5.673 0 1 0 0 11.346a5.673 5.673 0 0 0 0-11.346M7.125 9c-.055.127-.793 2.96-.793 2.96a.5.5 0 1 1-.966-.26s.88-2.827.88-3.43V6.801l-1.958-.525a.5.5 0 1 1 .258-.966s1.654.563 2.3.563h1.309c.645 0 2.298-.563 2.298-.563a.5.5 0 1 1 .26.966l-1.966.527V8.27c0 .603.88 3.427.88 3.427a.5.5 0 0 1-.966.259S7.92 9.127 7.869 9c-.05-.127-.219-.127-.219-.127h-.307s-.173 0-.218.127M7.5 5.12a1.125 1.125 0 1 0 0-2.25a1.125 1.125 0 0 0 0 2.25" clip-rule="evenodd"></path>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

3
schema.json Normal file
View File

@ -0,0 +1,3 @@
Need to install the following packages:
get-graphql-schema@2.1.2
Ok to proceed? (y)

3
server/tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

171
stores/main.ts Normal file
View File

@ -0,0 +1,171 @@
import { defineStore } from 'pinia'
interface CompanyLogo {
url: string
alternativeText?: string
}
interface CompanyInfo {
company: string
street: string
postalcode: string
city: string
phone: string
email: string
contact: string
district?: string
latitude?: number
longitude?: number
web: string
invertlogo?: {
data?: {
attributes?: CompanyLogo
}
}
}
interface Page {
id: number
pageName: string
pageLink: string
header_image?: {
url: string
alternativeText?: string
} | null
SEO?: {
pageTitle: string
seoDescription: string
seoKeywords: string
type: string
seoImage?: {
url: string
alternativeText?: string
} | null
} | null
faqs: Array<{
question: string
answer: string
}>
pageSections: Array<{
id: number
sectionText: string
sectionImage?: {
url: string
alternativeText?: string
} | null
}>
}
export const useMainStore = defineStore('main', {
state: () => ({
menuOpen: false,
contactBoxOpen: false,
scrollPosition: 0,
screenWidth: 1440,
companyinfo: null as CompanyInfo | null,
pages: [] as Page[],
dataFetched: false,
loading: false,
error: null as { message: string, stack?: string } | null,
}),
getters: {
invertLogoUrl: (state) => {
const runtimeConfig = useRuntimeConfig()
const logoUrl = state.companyinfo?.invertlogo?.data?.attributes?.url
return logoUrl
? `${runtimeConfig.public.cmsBaseUrl}${logoUrl}`
: '/uploads/dummy_Image_4abc3f04dd.webp'
},
isMobile: (state) => state.screenWidth < 768,
/** Neuer Getter: Seite anhand pageLink finden */
getPageByLink: (state) => {
return (link: string) => state.pages.find(p => p.pageLink === 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
},
async fetchInitialData() {
if (this.dataFetched) return
this.loading = true
try {
const runtimeConfig = useRuntimeConfig()
const cmsUrl = runtimeConfig.public.cmsBaseUrl
const companyRes = await $fetch(`${cmsUrl}/api/companyinfo?populate=*`, {
headers: { 'Authorization': `Bearer ${runtimeConfig.public.cmsToken}` }
})
this.companyinfo = companyRes.data?.attributes || companyRes
const pagesRes = await $fetch(`${cmsUrl}/api/pages?populate=*`, {
headers: { 'Authorization': `Bearer ${runtimeConfig.public.cmsToken}` }
})
this.pages = pagesRes.data.map((item: any) => ({
id: item.id,
pageName: item.attributes.pageName,
pageLink: item.attributes.pageLink,
header_image: item.attributes.header_image ? {
url: item.attributes.header_image.data.attributes.url,
alternativeText: item.attributes.header_image.data.attributes.alternativeText
} : null,
SEO: item.attributes.SEO ? {
pageTitle: item.attributes.SEO.pageTitle,
seoDescription: item.attributes.SEO.seoDesicription,
seoKeywords: item.attributes.SEO.seoKeywords,
type: item.attributes.SEO.type,
seoImage: item.attributes.SEO.seoImage ? {
url: item.attributes.SEO.seoImage.data.attributes.url,
alternativeText: item.attributes.SEO.seoImage.data.attributes.alternativeText
} : null
} : null,
faqs: item.attributes.faqs ? item.attributes.faqs.data.map((faq: any) => ({
question: faq.attributes.question,
answer: faq.attributes.answer
})) : [],
pageSections: item.attributes.pageSections ? item.attributes.pageSections.map((section: any) => ({
id: section.id,
sectionText: section.sectionText,
sectionImage: section.sectionImage ? {
url: section.sectionImage.data.attributes.url,
alternativeText: section.sectionImage.data.attributes.alternativeText
} : null
})) : []
}))
this.dataFetched = true
} catch (err) {
const errorObj = err as Error
this.error = {
message: errorObj.message,
stack: errorObj.stack
}
console.error('Fehler beim Laden der Daten:', errorObj)
} finally {
this.loading = false
}
}
}
})

4
tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}