init
This commit is contained in:
commit
fb73b7d2b5
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
|
||||
75
README.md
Normal file
75
README.md
Normal 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
34
app.vue
Normal 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>
|
||||
BIN
assets/fonts/Assistant-Regular.ttf
Normal file
BIN
assets/fonts/Assistant-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/ColabThi.otf
Normal file
BIN
assets/fonts/ColabThi.otf
Normal file
Binary file not shown.
BIN
assets/fonts/Comfortaa-Bold.ttf
Normal file
BIN
assets/fonts/Comfortaa-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Comfortaa-Light.ttf
Normal file
BIN
assets/fonts/Comfortaa-Light.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Comfortaa-Regular.ttf
Normal file
BIN
assets/fonts/Comfortaa-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/JMH_Typewriter.ttf
Normal file
BIN
assets/fonts/JMH_Typewriter.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-Black.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-Black.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-BlackItalic.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-BlackItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-Bold.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-Bold.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-BoldItalic.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-BoldItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-ExtraBold.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-ExtraBold.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-ExtraBoldItalic.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-ExtraBoldItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-ExtraLight.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-ExtraLight.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-ExtraLightItalic.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-ExtraLightItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-Italic.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-Italic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-Light.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-Light.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-LightItalic.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-LightItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-Medium.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-Medium.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-MediumItalic.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-MediumItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-Regular.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-Regular.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-SemiBold.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-SemiBold.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-SemiBoldItalic.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-SemiBoldItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-Thin.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-Thin.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/Montserrat-ThinItalic.otf
Normal file
BIN
assets/fonts/montserrat/Montserrat-ThinItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-Black.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-Black.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-BlackItalic.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-BlackItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-Bold.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-Bold.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-BoldItalic.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-BoldItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-ExtraBold.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-ExtraBold.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-ExtraBoldItalic.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-ExtraBoldItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-ExtraLight.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-ExtraLight.otf
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-Italic.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-Italic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-Light.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-Light.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-LightItalic.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-LightItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-Medium.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-Medium.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-MediumItalic.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-MediumItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-Regular.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-Regular.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-SemiBold.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-SemiBold.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-SemiBoldItalic.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-SemiBoldItalic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-Thin.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-Thin.otf
Normal file
Binary file not shown.
BIN
assets/fonts/montserrat/MontserratAlternates-ThinItalic.otf
Normal file
BIN
assets/fonts/montserrat/MontserratAlternates-ThinItalic.otf
Normal file
Binary file not shown.
43
assets/fonts/montserrat/SIL Open Font License.txt
Normal file
43
assets/fonts/montserrat/SIL Open Font License.txt
Normal 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.
|
||||
1
assets/images/DML_Puzzle.svg
Normal file
1
assets/images/DML_Puzzle.svg
Normal 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 |
9
assets/styles/accessability.sass
Normal file
9
assets/styles/accessability.sass
Normal file
@ -0,0 +1,9 @@
|
||||
body.high-contrast
|
||||
background-color: black
|
||||
color: white
|
||||
|
||||
html
|
||||
--text-size: 16px
|
||||
|
||||
body
|
||||
font-size: var(--text-size)
|
||||
262
assets/styles/bootstrap.sass
Normal file
262
assets/styles/bootstrap.sass
Normal 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
187
assets/styles/main.sass
Normal 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)
|
||||
|
||||
276
components/AccessabilityBox.vue
Normal file
276
components/AccessabilityBox.vue
Normal 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
219
components/Accordion.vue
Normal 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
116
components/Breadcrumbs.vue
Normal 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
446
components/ContactForm.vue
Normal 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
61
components/FAQArea.vue
Normal 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>
|
||||
|
||||
112
components/ImageComparisonSlider.vue
Normal file
112
components/ImageComparisonSlider.vue
Normal 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>
|
||||
|
||||
85
components/LanguageBox.vue
Normal file
85
components/LanguageBox.vue
Normal 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>
|
||||
286
components/MarqueeBanner.vue
Normal file
286
components/MarqueeBanner.vue
Normal 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>
|
||||
|
||||
30
components/SettingsPanel.vue
Normal file
30
components/SettingsPanel.vue
Normal 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>
|
||||
243
components/template/PageFooter.vue
Normal file
243
components/template/PageFooter.vue
Normal 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">
|
||||
© 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>
|
||||
319
components/template/PageHeader.vue
Normal file
319
components/template/PageHeader.vue
Normal 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>
|
||||
122
composables/useHTMLConverter.ts
Normal file
122
composables/useHTMLConverter.ts
Normal 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 };
|
||||
}
|
||||
|
||||
43
composables/usePageMeta.ts
Normal file
43
composables/usePageMeta.ts
Normal 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
6
eslint.config.mjs
Normal 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
102
i18n/locales/de.json
Normal 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
102
i18n/locales/en.json
Normal 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, you’ll learn exactly what steps are needed to turn your website into a powerful marketing tool.",
|
||||
"button": "Request your free initial consultation!"
|
||||
},
|
||||
"canDo": {
|
||||
"title": "Start using your website’s full potential!",
|
||||
"item1": {
|
||||
"title": "Gain new customers and increase revenue",
|
||||
"text": "Turn visitors into paying customers! With a clear strategy, compelling design and optimized user experience, your website becomes a lead machine."
|
||||
},
|
||||
"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 client’s relaunch process.",
|
||||
"text": "We place great emphasis on a clean design that aligns with users’ mental models – so visitors always find exactly what they’re looking for, right where they expect it."
|
||||
},
|
||||
"finalCall": {
|
||||
"title": "Together, we’ll take your business to the next level!",
|
||||
"button": "Contact us!"
|
||||
},
|
||||
"marqueeBanner": {
|
||||
"title": "These companies trust us"
|
||||
},
|
||||
"faqArea": {
|
||||
"headline": "Here you’ll find answers to frequently asked questions (FAQs) about website creation with digimedialoop"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
i18n/locales/es.json
Normal file
50
i18n/locales/es.json
Normal 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
50
i18n/locales/fr.json
Normal 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
50
i18n/locales/it.json
Normal 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
50
i18n/locales/tr.json
Normal 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
191
layouts/default.vue
Normal 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
152
nuxt.config.ts
Normal 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
17108
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
package.json
Normal file
38
package.json
Normal 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
15
pages/imprint/index.vue
Normal 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
347
pages/index.vue
Normal 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
15
pages/magazin/index.vue
Normal 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
15
pages/privacy/index.vue
Normal 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>
|
||||
15
pages/references/index.vue
Normal file
15
pages/references/index.vue
Normal 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
15
pages/services/index.vue
Normal 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
15
pages/terms/index.vue
Normal 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
18
pages/webagency/index.vue
Normal 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
4
plugins/store.server.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
const store = useMainStore()
|
||||
await store.fetchInitialData()
|
||||
})
|
||||
2
public/_robots.txt
Normal file
2
public/_robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
45
public/assets/icons/collection.svg
Normal file
45
public/assets/icons/collection.svg
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
3
schema.json
Normal file
3
schema.json
Normal 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
3
server/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
||||
171
stores/main.ts
Normal file
171
stores/main.ts
Normal 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
4
tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user