article carousel
This commit is contained in:
parent
51d9f75853
commit
8a72eafe4b
112
components/ArticleCard.vue
Normal file
112
components/ArticleCard.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<!-- components/ArticleCard.vue -->
|
||||
<template>
|
||||
<div class="article">
|
||||
<NuxtLinkLocale
|
||||
:to="link"
|
||||
class="article-link"
|
||||
>
|
||||
<div class="image-wrapper">
|
||||
<NuxtImg
|
||||
v-if="image?.url"
|
||||
:src="image.url"
|
||||
provider="strapi"
|
||||
:alt="image.alternativeText"
|
||||
format="webp"
|
||||
class="article-image"
|
||||
/>
|
||||
<div class="overlay">
|
||||
<h2>{{ header }}</h2>
|
||||
</div>
|
||||
<button class="btn mintBtn">{{ readmoreText }}</button>
|
||||
</div>
|
||||
</NuxtLinkLocale>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
header: String,
|
||||
image: Object,
|
||||
link: String,
|
||||
readmoreText: {
|
||||
type: String,
|
||||
default: 'Weiterlesen'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
.article
|
||||
width: 100%
|
||||
max-width: 500px
|
||||
border: 1px solid $beige
|
||||
background: linear-gradient(to bottom right, white, $lightgrey)
|
||||
border-radius: 1rem
|
||||
position: relative
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: flex-start
|
||||
overflow: hidden
|
||||
cursor: pointer
|
||||
transition: transform 0.3s ease, opacity 0.3s ease
|
||||
|
||||
&:hover
|
||||
transform: scale(1.05)
|
||||
|
||||
.article-link
|
||||
position: relative
|
||||
display: block
|
||||
color: white
|
||||
text-decoration: none
|
||||
|
||||
.image-wrapper
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 220px
|
||||
border: 1px solid $lightgrey
|
||||
|
||||
.article-image
|
||||
width: 100%
|
||||
min-height: 500px
|
||||
object-fit: cover
|
||||
border-top-left-radius: 1rem
|
||||
border-top-right-radius: 1rem
|
||||
opacity: .6
|
||||
|
||||
button
|
||||
position: absolute
|
||||
bottom: .6rem
|
||||
right: 0rem
|
||||
border: 1px solid $darkgrey
|
||||
font-size: 1rem
|
||||
box-shadow: 1px 1px 4px 2px rgba(black, .2)
|
||||
background-color: lighten($darkgrey, 10%)
|
||||
letter-spacing: .05rem
|
||||
|
||||
.overlay
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 80%
|
||||
height: auto
|
||||
min-height: 80%
|
||||
background-image: linear-gradient(to bottom right, rgba(darken(white, 0), 1), rgba(darken($beige, 0), 0.9))
|
||||
margin: 0 20% 7rem 0
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
justify-content: flex-start
|
||||
padding: 1rem
|
||||
border-top-left-radius: 1rem
|
||||
border-bottom-right-radius: 50%
|
||||
box-shadow: 2px 2px 5px 3px rgba(black, .2)
|
||||
|
||||
h2
|
||||
color: $darkgrey
|
||||
font-size: 1rem
|
||||
line-height: 140%
|
||||
font-family: 'Mainfont-Bold'
|
||||
margin: .2rem 1rem
|
||||
hyphens: auto
|
||||
</style>
|
||||
|
||||
171
components/ArticleCarousel.vue
Normal file
171
components/ArticleCarousel.vue
Normal file
@ -0,0 +1,171 @@
|
||||
<!-- components/ArticleCarousel.vue -->
|
||||
<template>
|
||||
<div class="carousel-wrapper">
|
||||
<button class="nav-button left" @click="prev">‹</button>
|
||||
<div class="carousel">
|
||||
<transition-group
|
||||
:name="directionClass"
|
||||
tag="div"
|
||||
class="carousel-inner"
|
||||
>
|
||||
|
||||
<div
|
||||
v-for="(article, index) in visibleItems"
|
||||
:key="article.id + '-' + currentIndex"
|
||||
class="carousel-item"
|
||||
>
|
||||
<ArticleCard
|
||||
:header="article.header"
|
||||
:image="article.image"
|
||||
:link="localePath({ name: 'article-link', params: { link: article.slug } })"
|
||||
:readmore-text="$t('pages.magazin.readmore')"
|
||||
/>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
<button class="nav-button right" @click="next">›</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ArticleCard from './ArticleCard.vue'
|
||||
import { useLocalePath } from '#i18n'
|
||||
|
||||
const props = defineProps({
|
||||
articles: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const localePath = useLocalePath()
|
||||
|
||||
const currentIndex = ref(0)
|
||||
const itemsPerView = ref(3)
|
||||
const direction = ref<'left' | 'right'>('right')
|
||||
|
||||
const directionClass = computed(() =>
|
||||
direction.value === 'left' ? 'slide-left' : 'slide-right'
|
||||
)
|
||||
|
||||
const updateItemsPerView = () => {
|
||||
const width = window.innerWidth
|
||||
if (width < 600) itemsPerView.value = 1
|
||||
else if (width < 1024) itemsPerView.value = 2
|
||||
else itemsPerView.value = 3
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateItemsPerView()
|
||||
window.addEventListener('resize', updateItemsPerView)
|
||||
})
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
const result = []
|
||||
for (let i = 0; i < itemsPerView.value; i++) {
|
||||
const index = (currentIndex.value + i) % props.articles.length
|
||||
result.push(props.articles[index])
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
function prev() {
|
||||
direction.value = 'left'
|
||||
currentIndex.value = (currentIndex.value - 1 + props.articles.length) % props.articles.length
|
||||
}
|
||||
|
||||
function next() {
|
||||
direction.value = 'right'
|
||||
currentIndex.value = (currentIndex.value + 1) % props.articles.length
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.carousel-wrapper
|
||||
position: relative
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
gap: 1rem
|
||||
|
||||
.nav-button
|
||||
all: unset
|
||||
cursor: pointer
|
||||
font-size: 4rem
|
||||
color: lighten($darkgrey, 20%)
|
||||
font-weight: bold
|
||||
padding: 0.5rem 1rem
|
||||
border-radius: 1rem
|
||||
background-image: radial-gradient(white, transparent)
|
||||
transition: 0.3s
|
||||
|
||||
&:hover
|
||||
background-image: radial-gradient($beige, transparent)
|
||||
|
||||
.carousel
|
||||
overflow-x: clip
|
||||
overflow-y: visible
|
||||
max-width: 100%
|
||||
flex: 1
|
||||
|
||||
.carousel-inner
|
||||
display: flex
|
||||
gap: 2rem
|
||||
transition: all 0.5s ease
|
||||
align-items: stretch
|
||||
|
||||
.carousel-item
|
||||
flex: 1 0 calc(100% / 3)
|
||||
min-width: 0
|
||||
transition: transform 0.3s ease
|
||||
display: flex
|
||||
|
||||
// Slide nach rechts (Nächste)
|
||||
.slide-right-enter-active
|
||||
transition: transform 0.6s ease, opacity 0.6s ease
|
||||
|
||||
.slide-right-enter-from
|
||||
transform: translateX(100%) scale(0.8)
|
||||
opacity: 0
|
||||
|
||||
.slide-right-enter-to
|
||||
transform: translateX(0) scale(1)
|
||||
opacity: 1
|
||||
|
||||
.slide-right-leave-active
|
||||
transition: transform 0.4s ease
|
||||
|
||||
.slide-right-leave-from
|
||||
transform: translateX(0)
|
||||
opacity: 1
|
||||
|
||||
.slide-right-leave-to
|
||||
transform: translateX(-100%)
|
||||
opacity: 1
|
||||
|
||||
|
||||
// Slide nach links (Vorherige)
|
||||
.slide-left-enter-active
|
||||
transition: transform 0.6s ease, opacity 0.6s ease
|
||||
|
||||
.slide-left-enter-from
|
||||
transform: translateX(-100%) scale(0.8)
|
||||
opacity: 0
|
||||
|
||||
.slide-left-enter-to
|
||||
transform: translateX(0) scale(1)
|
||||
opacity: 1
|
||||
|
||||
.slide-left-leave-active
|
||||
transition: transform 0.4s ease
|
||||
|
||||
.slide-left-leave-from
|
||||
transform: translateX(0)
|
||||
opacity: 1
|
||||
|
||||
.slide-left-leave-to
|
||||
transform: translateX(100%)
|
||||
opacity: 1
|
||||
|
||||
</style>
|
||||
|
||||
@ -121,11 +121,11 @@
|
||||
},
|
||||
"webagency": {
|
||||
"hero": {
|
||||
"title": "Ihre Webagentur für strategische Webentwicklung und funktionales Webdesign in Herrsching am Ammersee",
|
||||
"subtitle": "Wir entwickeln Webseiten, die aus Besuchern Kunden machen!",
|
||||
"text1": "Wir stehen für professionelle, innovative und strategische Weblösungen und kombinieren technisches Know-how mit einem tiefen Verständnis für digitale Kommunikation, um Unternehmen online erfolgreich zu positionieren",
|
||||
"text2": "Unser Ansatz ist immer individuell: Jedes Projekt wird mit Sorgfalt, Weitblick und den neuesten Technologien realisiert. Dabei setzen wir auf eine enge Zusammenarbeit und maßgeschneiderte Lösungen, die zu unseren Kunden passen. Wir begleiten Unternehmen aus unterschiedlichsten Branchen – von kleinen Betrieben bis hin zu größeren Firmen – auf ihrem Weg zu einer erfolgreichen Online-Präsenz.",
|
||||
"text3": "Lassen Sie uns gemeinsam Ihre digitale Präsenz optimieren!",
|
||||
"title": "Ihre Webagentur in Herrsching am Ammersee",
|
||||
"subtitle": "Wir entwickeln strategische Webseiten mit funktionalem Design, die aus Besuchern Kunden machen!",
|
||||
"text1": "Digitale Sichtbarkeit beginnt mit einer Website, die mehr kann als gut aussehen: Sie wird zum zentralen Marketinginstrument: Vom MVP (Minimal Viable Product) bis hin zum ausgereiften Lead-Generator.",
|
||||
"text2": "Mit modernen Technologien wie Nuxt 3 und einem Headless CMS entstehen Weblösungen, die flexibel wachsen, effizient im Team gepflegt werden können und perfekt auf Kommunikations- und Marketingprozesse abgestimmt sind.",
|
||||
"text3": "Ob Unternehmen oder Agentur – im Fokus steht immer eine Lösung, die Ziele erreichbar macht: sichtbar werden, Vertrauen aufbauen, Kunden gewinnen.",
|
||||
"button": "Ihr Kontakt zu uns!"
|
||||
},
|
||||
"team": {
|
||||
|
||||
@ -1,50 +1,59 @@
|
||||
<template>
|
||||
<div v-if="article" class="article">
|
||||
<SideBarNaviSlider link="/magazin">
|
||||
{{ $t('pages.article.artikelUebersicht') }}
|
||||
</SideBarNaviSlider>
|
||||
<div class="articlePage">
|
||||
|
||||
<NuxtImg
|
||||
v-if="article.image?.url"
|
||||
:src="article.image.url"
|
||||
:alt="article.image.alternativeText || article.header"
|
||||
class="img_background"
|
||||
provider="strapi"
|
||||
role="img"
|
||||
/>
|
||||
<div v-if="article" class="article">
|
||||
<SideBarNaviSlider link="/magazin">
|
||||
{{ $t('pages.article.artikelUebersicht') }}
|
||||
</SideBarNaviSlider>
|
||||
|
||||
<div class="paper">
|
||||
<p class="articleInfo">
|
||||
<span><b>{{ $t('pages.article.autor') }}</b> {{ author }}</span> <span><b>{{ $t('pages.article.date') }}</b> {{ formattedDate }}</span>
|
||||
</p>
|
||||
<h1>{{ article.header }}</h1>
|
||||
<NuxtImg
|
||||
v-if="article.image?.url && screenWidth > 800"
|
||||
:src="article.image.url"
|
||||
:alt="article.image.alternativeText || article.header"
|
||||
class="img_article"
|
||||
provider="strapi"
|
||||
role="img"
|
||||
/>
|
||||
<p class="teaser">{{ article.teaser }}</p>
|
||||
<NuxtImg
|
||||
v-if="article.image?.url"
|
||||
:src="article.image.url"
|
||||
:alt="article.image.alternativeText || article.header"
|
||||
class="img_background"
|
||||
provider="strapi"
|
||||
role="img"
|
||||
/>
|
||||
|
||||
<div class="paper">
|
||||
<p class="articleInfo">
|
||||
<span><b>{{ $t('pages.article.autor') }}</b> {{ author }}</span> <span><b>{{ $t('pages.article.date') }}</b> {{ formattedDate }}</span>
|
||||
</p>
|
||||
<h1>{{ article.header }}</h1>
|
||||
<NuxtImg
|
||||
v-if="article.image?.url && screenWidth > 800"
|
||||
:src="article.image.url"
|
||||
:alt="article.image.alternativeText || article.header"
|
||||
class="img_article"
|
||||
provider="strapi"
|
||||
role="img"
|
||||
/>
|
||||
<p class="teaser">{{ article.teaser }}</p>
|
||||
|
||||
|
||||
<div class="content" v-html="htmlContent(article.content)"></div>
|
||||
<div class="content" v-html="htmlContent(article.content)"></div>
|
||||
|
||||
<button
|
||||
class="btn pinkBtn"
|
||||
role="button"
|
||||
aria-label="Kontakt aufnehmen"
|
||||
@click.prevent="toggleContactBubble"
|
||||
>
|
||||
{{ $t('pages.article.buttonText') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn pinkBtn"
|
||||
role="button"
|
||||
aria-label="Kontakt aufnehmen"
|
||||
@click.prevent="toggleContactBubble"
|
||||
>
|
||||
{{ $t('pages.article.buttonText') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-else class="container topSpace">
|
||||
<p>{{ $t('pages.article.ladenOderNichtGefunden') }}</p>
|
||||
</section>
|
||||
<section v-else class="container topSpace">
|
||||
<p>{{ $t('pages.article.ladenOderNichtGefunden') }}</p>
|
||||
</section>
|
||||
|
||||
<div class="container-5">
|
||||
<h3>Vielleicht sind diese Artikel auch Interessant?</h3>
|
||||
<ArticleCarousel :articles="recommendedArticles" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -63,11 +72,20 @@
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
const route = useRoute()
|
||||
const slug = route.params.link
|
||||
console.log(slug)
|
||||
|
||||
const slug = ref(route.params.link)
|
||||
watch(() => route.params.link, (newSlug) => {
|
||||
slug.value = newSlug
|
||||
})
|
||||
|
||||
|
||||
const mainStore = useMainStore()
|
||||
const { articles } = storeToRefs(mainStore)
|
||||
const { articles, getRecommendedArticles } = storeToRefs(mainStore)
|
||||
|
||||
const recommendedArticles = computed(() => {
|
||||
if (!article.value) return []
|
||||
return getRecommendedArticles.value(article.value)
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@ -76,7 +94,7 @@
|
||||
// Artikel suchen
|
||||
const article = computed(() => {
|
||||
if (!articles.value) return null
|
||||
return articles.value.find(item => item.slug === slug) ?? null
|
||||
return articles.value.find(item => item.slug === slug.value) ?? null
|
||||
})
|
||||
|
||||
const { convertToHTML } = useHtmlConverter()
|
||||
@ -182,7 +200,7 @@
|
||||
.paper
|
||||
background-color: rgba(white, .98)
|
||||
width: 88%
|
||||
margin: 10rem 6% 15vh 6%
|
||||
margin: 10rem 6% 3rem 6%
|
||||
position: relative
|
||||
z-index: 1
|
||||
border-radius: 1rem
|
||||
|
||||
@ -33,32 +33,16 @@
|
||||
class="grid"
|
||||
appear
|
||||
>
|
||||
<div
|
||||
<ArticleCard
|
||||
v-for="article in filteredArticles"
|
||||
:key="article.id"
|
||||
class="article"
|
||||
>
|
||||
<NuxtLinkLocale
|
||||
:to="localePath({ name: 'article-link', params: { link: article.slug } })"
|
||||
class="article-link"
|
||||
>
|
||||
<div class="image-wrapper">
|
||||
<NuxtImg
|
||||
v-if="article.image?.url"
|
||||
:src="article.image.url"
|
||||
provider="strapi"
|
||||
:alt="article.image.alternativeText"
|
||||
format="webp"
|
||||
class="article-image"
|
||||
/>
|
||||
<div class="overlay">
|
||||
<h2>{{ article.header }}</h2>
|
||||
</div>
|
||||
<button class="btn mintBtn">{{ $t('pages.magazin.readmore') }}</button>
|
||||
</div>
|
||||
</NuxtLinkLocale>
|
||||
</div>
|
||||
:header="article.header"
|
||||
:image="article.image"
|
||||
:link="localePath({ name: 'article-link', params: { link: article.slug } })"
|
||||
:readmore-text="$t('pages.magazin.readmore')"
|
||||
/>
|
||||
</transition-group>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
@ -66,6 +50,14 @@
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
sitemap: {
|
||||
lastmod: '2025-06-23', // ISO 8601 – oder dynamisch generieren
|
||||
changefreq: 'daily', // optional
|
||||
priority: 1 // optional
|
||||
}
|
||||
})
|
||||
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useLocalePath } from '#i18n'
|
||||
@ -178,22 +170,7 @@ watch(articles, (newVal) => {
|
||||
margin: 0 auto
|
||||
transition: all 0.3s ease-in-out // weichere Umordnung der Items
|
||||
|
||||
.article
|
||||
width: 100%
|
||||
max-width: 500px
|
||||
border: 1px solid $beige
|
||||
background: linear-gradient(to bottom right, white, $lightgrey)
|
||||
border-radius: 1rem
|
||||
position: relative
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: flex-start
|
||||
overflow: hidden
|
||||
cursor: pointer
|
||||
transition: transform 0.3s ease, opacity 0.3s ease
|
||||
|
||||
&:hover
|
||||
transform: scale(1.05)
|
||||
|
||||
.article-enter-from,
|
||||
.article-leave-to
|
||||
@ -205,76 +182,5 @@ watch(articles, (newVal) => {
|
||||
transition: all 0.3s ease
|
||||
|
||||
|
||||
.article-link
|
||||
position: relative
|
||||
display: block
|
||||
color: white
|
||||
text-decoration: none
|
||||
|
||||
.image-wrapper
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 220px
|
||||
border: 1px solid $lightgrey
|
||||
|
||||
.article-image
|
||||
width: 100%
|
||||
height: 500px
|
||||
object-fit: cover
|
||||
border-top-left-radius: 1rem
|
||||
border-top-right-radius: 1rem
|
||||
opacity: .6
|
||||
|
||||
button
|
||||
position: absolute
|
||||
bottom: .6rem
|
||||
right: 0rem
|
||||
border: 1px solid $darkgrey
|
||||
font-size: 1rem
|
||||
box-shadow: 1px 1px 4px 2px rgba(black, .2)
|
||||
background-color: lighten($darkgrey, 10%)
|
||||
letter-spacing: .05rem
|
||||
|
||||
|
||||
.overlay
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 80%
|
||||
height: auto
|
||||
min-height: 80%
|
||||
background-image: linear-gradient(to bottom right, rgba(darken(white, 0), 1), rgba(darken($beige, 0), 0.9) )
|
||||
margin: 0 20% 7rem 0
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
justify-content: flex-start
|
||||
padding: 1rem
|
||||
border-top-left-radius: 1rem
|
||||
border-bottom-right-radius: 50%
|
||||
//border: 1px solid darken($beige, 10%)
|
||||
//border-radius: .5rem
|
||||
text-align: left
|
||||
transition: .3s
|
||||
box-shadow: 2px 2px 5px 3px rgba(black, .2)
|
||||
|
||||
|
||||
h2
|
||||
color: $darkgrey
|
||||
font-size: 1rem
|
||||
line-height: 140%
|
||||
font-family: 'Mainfont-Bold'
|
||||
margin: .2rem 1rem
|
||||
//text-transform: uppercase
|
||||
hyphens: auto
|
||||
|
||||
.mintBtn
|
||||
background-color: $primaryColor
|
||||
color: white
|
||||
font-size: 0.9rem
|
||||
font-family: 'Mainfont-Bold'
|
||||
border: none
|
||||
padding: 0.4rem 1rem
|
||||
border-radius: 0.3rem
|
||||
cursor: pointer
|
||||
</style>
|
||||
@ -18,21 +18,53 @@
|
||||
alt=""
|
||||
/>
|
||||
<div class="textBox">
|
||||
<h2>Sie möchten sich eine neue Webseite für Ihre Schule oder Ihren Kindergarten erstellen lassen?</h2>
|
||||
<h2>Sie benötigen eine neue Webseite für Ihre Schule oder Ihren Kindergarten?</h2>
|
||||
|
||||
<p> Wir entwickeln moderne, barrierefreie Webseiten, die Eltern, Schüler und Mitarbeitende klar informieren und sich ganz einfach pflegen lassen. </p>
|
||||
<p>Erfahren Sie mehr in unseren beiden Fachartikeln:
|
||||
<ul>
|
||||
<li>„<a href="/artikel/auf-was-man-bei-der-erstellung-von-schulwebseiten-achten-sollte" target="_blank">Worauf es bei Webseiten im Bildungsbereich ankommt</a>“</li>
|
||||
<li>„<a href="/artikel/digitale-elternkommunikation-welche-rolle-spielt-die-schulhomepage" target="_blank">Digitale Elternkommunikation: Welche Rolle spielt die Schulhomepage?</a>“</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p> <b>Dann sind Sie hier goldrichtig!</b> Denn wir erstellen moderne, barrierefreie Webseiten, die Eltern, Schüler und Mitarbeitende klar informieren und sich ganz einfach pflegen lassen. </p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="forWhom">
|
||||
<NuxtImg
|
||||
provider="strapi"
|
||||
src="/uploads/watercolor_da8a37ce48.webp"
|
||||
alt=""
|
||||
class="accessibility-bg"
|
||||
sizes="100vw"
|
||||
priority
|
||||
/>
|
||||
<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"/>
|
||||
</svg>
|
||||
<div class="container-20">
|
||||
<h2>Unsere Weblösungen richten sich unter anderem an folgende Bildungseinrichtungen:</h2>
|
||||
<ul class="check">
|
||||
<li>Grundschulen</li>
|
||||
<li>weiterführende Schulen (z. B. Gymnasien, Realschulen, Mittelschulen, Gesamtschule)</li>
|
||||
<li>Förderschulen</li>
|
||||
<li>Berufsschulen und Berufsfachschulen</li>
|
||||
<li>Kindergärten und Kitas</li>
|
||||
<li>Horte und Ganztagseinrichtungen</li>
|
||||
<li>Volkshochschulen</li>
|
||||
<li>Musikschulen</li>
|
||||
<li>Familienzentren</li>
|
||||
<li>Bildungsträger und Akademien</li>
|
||||
</ul>
|
||||
</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"/>
|
||||
</svg>
|
||||
</section>
|
||||
|
||||
<section class="articleBox container-5">
|
||||
<h3>Wissenswertes rund um die Webseite für Bildungseinrichtungen</h3>
|
||||
<ArticleCarousel :articles="recommendedArticles" />
|
||||
</section>
|
||||
|
||||
|
||||
<section class="accessibility">
|
||||
<NuxtImg
|
||||
provider="strapi"
|
||||
@ -46,7 +78,7 @@
|
||||
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"/>
|
||||
</svg>
|
||||
<div class="paper">
|
||||
<h2>Barrierefreiheit ist für Bildungseinrichtungen in Deutschland Pflicht!</h2>
|
||||
<h2>Schon gewusst? - Barrierefreiheit ist für Bildungseinrichtungen in Deutschland Pflicht!</h2>
|
||||
<p>
|
||||
Wir erstellen Ihre Schulhomepage von Beginn an nach den <b>Anforderungen der Barrierefreiheit (BITV 2.0)</b> und sorgen dafür, dass Ihre Seite auch dann barrierefrei bleibt, wenn Inhalte später von Ihnen oder dem Kollegium selbst eingepflegt werden.
|
||||
</p>
|
||||
@ -154,7 +186,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const mainStore = useMainStore()
|
||||
const { getArticlesByCategoryIds } = storeToRefs(mainStore)
|
||||
|
||||
const recommendedArticles = computed(() => {
|
||||
return getArticlesByCategoryIds.value([5])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@ -163,6 +202,8 @@
|
||||
hyphens: auto
|
||||
.weKnow
|
||||
position: relative
|
||||
padding-top: 2rem
|
||||
padding-bottom: 2rem
|
||||
img
|
||||
position: absolute
|
||||
top: -5%
|
||||
@ -172,7 +213,7 @@
|
||||
z-index: 4
|
||||
.textBox
|
||||
z-index: 5
|
||||
margin-left: 25%
|
||||
margin-left: 20%
|
||||
max-width: 700px
|
||||
width: 55%
|
||||
h2
|
||||
@ -187,33 +228,43 @@
|
||||
line-height: 140%
|
||||
font-size: 1.3rem
|
||||
|
||||
.forWhom
|
||||
margin: 3rem auto
|
||||
padding-top: 4rem
|
||||
padding-bottom: 4rem
|
||||
|
||||
.articleBox
|
||||
margin-top: 5rem
|
||||
margin-bottom: 6rem
|
||||
h3
|
||||
text-align: center
|
||||
|
||||
|
||||
.accessibility
|
||||
position: relative
|
||||
padding: 5rem 0
|
||||
padding: 4rem 0
|
||||
overflow: hidden
|
||||
|
||||
.accessibility-bg
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
object-fit: cover
|
||||
object-position: center
|
||||
opacity: 0.15 // sehr sanft, nicht störend
|
||||
z-index: -1
|
||||
pointer-events: none
|
||||
|
||||
.paper
|
||||
padding: 2rem 5%
|
||||
padding: 1rem 5%
|
||||
background-color: rgba(white, .8)
|
||||
margin: 4rem 10%
|
||||
margin: 1rem 10%
|
||||
transform: rotate(-1deg)
|
||||
border: 1px solid $lightgrey
|
||||
box-shadow: 2px 2px 10px 2px #EEE
|
||||
h2
|
||||
font-size: 1.8rem
|
||||
|
||||
.accessibility-bg
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
object-fit: cover
|
||||
object-position: center
|
||||
opacity: 0.15 // sehr sanft, nicht störend
|
||||
z-index: -1
|
||||
pointer-events: none
|
||||
.cmsAdmin
|
||||
text-align: center
|
||||
.textBox
|
||||
|
||||
@ -80,13 +80,24 @@ class="btn pinkBtn" role="button"
|
||||
|
||||
</section>
|
||||
|
||||
<CallToActionBox
|
||||
headline="Sie brauchen einen Barrierefreiheitscheck?"
|
||||
text="Kein Problem! Wir schauen uns Ihre Seite gerne an..."
|
||||
button-text="Kontaktieren Sie uns!"
|
||||
/>
|
||||
|
||||
<section class="articleBox container-5">
|
||||
<h3>Lesen Sie jetzt mehr zum Thema Barrierefreiheit und Datenschutz im Web</h3>
|
||||
<ArticleCarousel :articles="recommendedArticles" />
|
||||
</section>
|
||||
|
||||
<FAQArea page-link="/leistungen/barrierefreie-webseiten" headline="Weitere Fragen zum Thema Barrierefreiheit im Web" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useMainStore } from '@/stores/main';
|
||||
|
||||
definePageMeta({
|
||||
alias: [
|
||||
'/leistungen/barrierefreie-webseiten', // Deutsch
|
||||
@ -96,14 +107,31 @@ definePageMeta({
|
||||
'/servicios/accesibilidad', // Spanisch
|
||||
'/hizmetler/erisilebilirlik' // Türkisch
|
||||
],
|
||||
name: 'services-accessibility'
|
||||
name: 'services-accessibility',
|
||||
sitemap: {
|
||||
lastmod: '2025-06-23', // ISO 8601 – oder dynamisch generieren
|
||||
changefreq: 'monthly', // optional
|
||||
priority: 1 // optional
|
||||
}
|
||||
})
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const mainStore = useMainStore()
|
||||
const { getArticlesByCategoryIds } = storeToRefs(mainStore)
|
||||
|
||||
const recommendedArticles = computed(() => {
|
||||
return getArticlesByCategoryIds.value([1])
|
||||
})
|
||||
const mainStore = useMainStore();
|
||||
const toggleContactBubble = () => mainStore.toggleContactBubble();
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.accessiblityPage
|
||||
.articleBox
|
||||
margin-top: 5rem
|
||||
margin-bottom: 6rem
|
||||
h3
|
||||
text-align: center
|
||||
.legalBasis
|
||||
background: linear-gradient(90deg, #39324A 0%, #403871 100%);
|
||||
color: white
|
||||
|
||||
@ -105,6 +105,17 @@
|
||||
</svg>
|
||||
</section>
|
||||
|
||||
<CallToActionBox
|
||||
headline="Sich wollen eine KI-kompatible Webseite?"
|
||||
text="Lassen Sie uns gemeinsam durchstarten."
|
||||
button-text="Wir beraten Sie gerne!"
|
||||
/>
|
||||
|
||||
<section class="articleBox container-5">
|
||||
<h3>Lesen Sie jetzt mehr zur Suchmaschinen-Optimierung in unserem Fachmagazin</h3>
|
||||
<ArticleCarousel :articles="recommendedArticles" />
|
||||
</section>
|
||||
|
||||
<FAQArea
|
||||
page-link="/leistungen/ki-kompatible-webseiten"
|
||||
:headline="$t('pages.services.ai.faq.headline')"
|
||||
@ -113,8 +124,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useMainStore } from '@/stores/main'
|
||||
const mainStore = useMainStore()
|
||||
|
||||
definePageMeta({
|
||||
alias: [
|
||||
'/leistungen/ki-kompatible-webseiten', // Deutsch
|
||||
@ -124,12 +134,31 @@ definePageMeta({
|
||||
'/servicios/sitios-compatibles-ai', // Spanisch
|
||||
'/hizmetler/ai-uyumlu-websiteler' // Türkisch
|
||||
],
|
||||
name: 'services-ai'
|
||||
name: 'services-ai',
|
||||
sitemap: {
|
||||
lastmod: '2025-06-23', // ISO 8601 – oder dynamisch generieren
|
||||
changefreq: 'monthly', // optional
|
||||
priority: 1 // optional
|
||||
}
|
||||
})
|
||||
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const mainStore = useMainStore()
|
||||
const { getArticlesByCategoryIds } = storeToRefs(mainStore)
|
||||
|
||||
const recommendedArticles = computed(() => {
|
||||
return getArticlesByCategoryIds.value([4])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.aiPage
|
||||
.articleBox
|
||||
margin-top: 5rem
|
||||
margin-bottom: 6rem
|
||||
h3
|
||||
text-align: center
|
||||
.targetGroup
|
||||
background: linear-gradient(90deg, #3A283E 0%, #6A3385 100%);
|
||||
color: white
|
||||
|
||||
@ -140,7 +140,12 @@
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
name: 'services-cms'
|
||||
name: 'services-cms',
|
||||
sitemap: {
|
||||
lastmod: '2025-06-23', // ISO 8601 – oder dynamisch generieren
|
||||
changefreq: 'monthly', // optional
|
||||
priority: 1 // optional
|
||||
}
|
||||
})
|
||||
|
||||
import { useMainStore } from '@/stores/main';
|
||||
|
||||
@ -120,6 +120,11 @@
|
||||
button-text="Sprechen Sie mit uns!"
|
||||
/>
|
||||
|
||||
<section class="articleBox container-5">
|
||||
<h3>Lesen Sie jetzt mehr zur Suchmaschinen-Optimierung in unserem Fachmagazin</h3>
|
||||
<ArticleCarousel :articles="recommendedArticles" />
|
||||
</section>
|
||||
|
||||
<FAQArea
|
||||
page-link="/leistungen/suchmaschinenoptimierung"
|
||||
headline="Fragen und Antworten zur Suchmaschinen-Optimierung"
|
||||
@ -138,13 +143,31 @@ definePageMeta({
|
||||
'/servicios/optimizacion-motores-busqueda', // Spanisch
|
||||
'/hizmetler/arama-motoru-optimizasyonu' // Türkisch
|
||||
],
|
||||
name: 'services-seo'
|
||||
name: 'services-seo',
|
||||
sitemap: {
|
||||
lastmod: '2025-06-23', // ISO 8601 – oder dynamisch generieren
|
||||
changefreq: 'monthly', // optional
|
||||
priority: 1 // optional
|
||||
}
|
||||
})
|
||||
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const mainStore = useMainStore()
|
||||
const { getArticlesByCategoryIds } = storeToRefs(mainStore)
|
||||
|
||||
const recommendedArticles = computed(() => {
|
||||
return getArticlesByCategoryIds.value([2])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.seoPage
|
||||
.articleBox
|
||||
margin-top: 5rem
|
||||
margin-bottom: 6rem
|
||||
h3
|
||||
text-align: center
|
||||
ul
|
||||
padding-left: 1rem
|
||||
li
|
||||
|
||||
163
pages/tools/ContrastChecker.vue
Normal file
163
pages/tools/ContrastChecker.vue
Normal file
@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="contrastChecker">
|
||||
<div class="input-section">
|
||||
<label for="domain">Domain eingeben:</label>
|
||||
<div class="row">
|
||||
<input id="domain" v-model="domainInput" type="text" placeholder="z. B. example.com" />
|
||||
<button :disabled="!isValidDomain" @click="loadDomain">Anzeigen</button>
|
||||
</div>
|
||||
<p v-if="domainInput && !isValidDomain" class="error">Ungültige Domain</p>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
v-if="iframeSrc"
|
||||
:src="iframeSrc"
|
||||
class="preview-frame"
|
||||
sandbox="allow-same-origin allow-scripts"
|
||||
></iframe>
|
||||
|
||||
<div class="color-buttons">
|
||||
<button @click="pickColor('background')">Hintergrundfarbe wählen</button>
|
||||
<button @click="pickColor('text')">Schriftfarbe wählen</button>
|
||||
</div>
|
||||
|
||||
<div v-if="contrastRatio" class="result-box">
|
||||
<p><strong>Kontrast:</strong> {{ contrastRatio.toFixed(2) }}</p>
|
||||
<p><strong>WCAG Bewertung:</strong> {{ contrastLevel }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
|
||||
const domainInput = ref('')
|
||||
const iframeSrc = ref('')
|
||||
const bgColor = ref(null)
|
||||
const textColor = ref(null)
|
||||
|
||||
const domainRegex = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
const isValidDomain = computed(() => domainRegex.test(domainInput.value))
|
||||
|
||||
// Diese Funktionen hinzufügen
|
||||
function hexToRgb(hex) {
|
||||
if (!hex) return null
|
||||
let c = hex.replace('#', '')
|
||||
if (c.length === 3) {
|
||||
c = c.split('').map((char) => char + char).join('')
|
||||
}
|
||||
const bigint = parseInt(c, 16)
|
||||
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255]
|
||||
}
|
||||
|
||||
function luminance(r, g, b) {
|
||||
const a = [r, g, b].map(v => {
|
||||
v /= 255
|
||||
return v <= 0.03928
|
||||
? v / 12.92
|
||||
: Math.pow((v + 0.055) / 1.055, 2.4)
|
||||
})
|
||||
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2]
|
||||
}
|
||||
|
||||
function getContrast(bgHex, fgHex) {
|
||||
const bgRgb = hexToRgb(bgHex)
|
||||
const fgRgb = hexToRgb(fgHex)
|
||||
if (!bgRgb || !fgRgb) return null
|
||||
const L1 = luminance(...bgRgb)
|
||||
const L2 = luminance(...fgRgb)
|
||||
return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05)
|
||||
}
|
||||
function loadDomain() {
|
||||
iframeSrc.value = 'https://' + domainInput.value
|
||||
}
|
||||
|
||||
function pickColor(type) {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'color'
|
||||
input.style.display = 'none'
|
||||
input.addEventListener('input', () => {
|
||||
if (type === 'background') bgColor.value = input.value
|
||||
if (type === 'text') textColor.value = input.value
|
||||
})
|
||||
document.body.appendChild(input)
|
||||
input.click()
|
||||
input.remove()
|
||||
}
|
||||
|
||||
const contrastRatio = computed(() => {
|
||||
if (!bgColor.value || !textColor.value) return null
|
||||
try {
|
||||
return getContrast(bgColor.value, textColor.value)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const contrastLevel = computed(() => {
|
||||
if (!contrastRatio.value) return 'unbekannt'
|
||||
const ratio = contrastRatio.value
|
||||
if (ratio >= 7) return 'AAA'
|
||||
if (ratio >= 4.5) return 'AA'
|
||||
if (ratio >= 3) return 'A'
|
||||
return 'nicht ausreichend'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.contrastChecker
|
||||
max-width: 1200px
|
||||
margin: 10vh auto 3rem auto
|
||||
padding: 2rem
|
||||
|
||||
|
||||
.input-section
|
||||
margin-bottom: 1rem
|
||||
|
||||
.row
|
||||
display: flex
|
||||
gap: 0.5rem
|
||||
margin-top: 0.5rem
|
||||
|
||||
input[type="text"]
|
||||
flex: 1
|
||||
padding: 0.5rem
|
||||
border: 1px solid #ccc
|
||||
border-radius: 4px
|
||||
|
||||
button
|
||||
padding: 0.5rem 1rem
|
||||
border: none
|
||||
background-color: #007acc
|
||||
color: white
|
||||
cursor: pointer
|
||||
border-radius: 4px
|
||||
|
||||
button:disabled
|
||||
background-color: #ccc
|
||||
cursor: not-allowed
|
||||
|
||||
.error
|
||||
color: red
|
||||
font-size: 0.9rem
|
||||
margin-top: 0.25rem
|
||||
|
||||
.preview-frame
|
||||
width: 100%
|
||||
height: 300px
|
||||
border: 1px solid #ccc
|
||||
margin-top: 1rem
|
||||
|
||||
.color-buttons
|
||||
margin-top: 1rem
|
||||
display: flex
|
||||
gap: 1rem
|
||||
|
||||
.result-box
|
||||
margin-top: 1rem
|
||||
padding: 1rem
|
||||
border: 1px solid #ccc
|
||||
border-radius: 4px
|
||||
background: #f9f9f9
|
||||
|
||||
</style>
|
||||
@ -104,6 +104,14 @@
|
||||
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
sitemap: {
|
||||
lastmod: '2025-06-23', // ISO 8601 – oder dynamisch generieren
|
||||
changefreq: 'monthly', // optional
|
||||
priority: 0.8 // optional
|
||||
}
|
||||
})
|
||||
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useMainStore } from "@/stores/main";
|
||||
import { computed } from "vue";
|
||||
|
||||
@ -151,6 +151,25 @@ export const useMainStore = defineStore('main', {
|
||||
state.projects.find(project => project.link === link),
|
||||
getArticleBySlug: (state) => (slug: string) =>
|
||||
state.articles.find((a) => a.slug === slug),
|
||||
getRecommendedArticles: (state) => (article: NewsArticle) => {
|
||||
if (!article?.categories || !Array.isArray(state.articles)) return []
|
||||
|
||||
const categoryIds = article.categories.map(c => c.id)
|
||||
|
||||
return state.articles.filter(a => {
|
||||
if (a.id === article.id) return false // sich selbst ausschließen
|
||||
if (!a.categories) return false
|
||||
|
||||
return a.categories.some(cat => categoryIds.includes(cat.id))
|
||||
})
|
||||
},
|
||||
getArticlesByCategoryIds: (state) => (categoryIds: number[]) => {
|
||||
if (!Array.isArray(categoryIds)) return []
|
||||
|
||||
return state.articles.filter(article =>
|
||||
article.categories?.some(cat => categoryIds.includes(cat.id))
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user