magazin new categories

This commit is contained in:
Sabrina Hennrich 2025-06-20 15:36:08 +02:00
parent d2923187d5
commit bbe1f7382e
8 changed files with 218 additions and 554 deletions

View File

@ -140,14 +140,8 @@
}
const navigationLinks = [
{
routeKey: '',
label: 'menu.webagency',
subNav: [
{ routeKey: 'webagency', label: 'menu.menuAbout' },
{ routeKey: 'magazin', label: 'menu.menuMagazin' }
]
},
{ routeKey: 'webagency', label: 'menu.webagency' },
{ routeKey: 'magazin', label: 'menu.menuMagazin' },
{
routeKey: '',
label: 'menu.services',

View File

@ -1,436 +0,0 @@
<template>
<div
class="navigationBox"
:class="[
isMenuOpen ? 'menu-active' : '',
screenWidth < 1350 ? 'mobile' : 'desk'
]"
role="navigation"
aria-label="Hauptnavigation"
tabindex="0"
>
<div
class="closer"
role="button"
tabindex="0"
aria-label="Navigation schließen"
@click="toggleMenu"
@keydown.enter="toggleMenu"
/>
<nav
v-if="isMenuOpen || screenWidth > 1350"
:aria-expanded="screenWidth < 1350 ? 'true' : undefined"
@mouseleave="screenWidth >= 1350 && hideSubNav()"
>
<!-- <div class="mobilNavLogo">
<NuxtImg
v-if="screenWidth < 1350"
provider="strapi"
src="/uploads/DML_Logo_mint_negative_2024_9257db5430.svg"
alt="digimedialoop Logo"
width="120"
/>
</div> -->
<span
v-for="link in navigationLinks"
:key="link.routeKey"
class="main-nav-item"
@mouseenter="screenWidth >= 1350 && showSubNav(link.routeKey)"
@mouseleave="screenWidth >= 1350 && hideSubNav(link.routeKey)"
>
<NuxtLinkLocale
:to="link.routeKey"
class="main-nav-link"
:aria-haspopup="link.subNav && link.subNav.length > 0 ? 'true' : undefined"
:aria-expanded="isSubNavOpen === link.routeKey ? 'true' : 'false'"
@click="handleMobileClose"
>
{{ $t(link.label) }}
</NuxtLinkLocale>
<!-- PFEIL FÜR MOBILE UND TOGGLE -->
<button
v-if="link.subNav && link.subNav.length > 0 && screenWidth < 1350"
class="submenu-toggle"
@click.prevent="toggleMobileSubNav(link.routeKey)"
:aria-expanded="isMobileSubNavOpen === link.routeKey ? 'true' : 'false'"
aria-label="Untermenü öffnen/schließen"
>
<svg
:class="{ 'open': isMobileSubNavOpen === link.routeKey }"
xmlns="http://www.w3.org/2000/svg"
width="12"
height="8"
viewBox="0 0 12 8"
fill="none"
>
<path d="M1 1L6 6L11 1" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<!-- SUBNAVIGATION -->
<ul
v-if="link.subNav && link.subNav.length > 0"
v-show="(screenWidth >= 1350 && isSubNavOpen === link.routeKey) || (screenWidth < 1350 && isMobileSubNavOpen === link.routeKey)"
class="sub-nav"
>
<li v-for="subLink in link.subNav" :key="subLink.routeKey" class="sub-nav-item">
<NuxtLinkLocale
:to="subLink.routeKey"
class="sub-nav-link"
@click="handleMobileClose"
>
{{ $t(subLink.label) }}
</NuxtLinkLocale>
</li>
</ul>
</span>
<a
class="menu_link" href="#"
role="button"
aria-label="Kontaktformular öffnen"
@click="toggleContactBubble"
>
{{ $t('contact') }}
</a>
<SettingsPanel v-if="screenWidth < 1350" />
</nav>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useMainStore } from '@/stores/main'
const mainStore = useMainStore()
const screenWidth = computed(() => mainStore.screenWidth)
const scrollPosition = computed(() => mainStore.scrollPosition)
const isSubNavOpen = ref(null) // speichert, welches Submenü auf Desktop offen ist
const isMobileSubNavOpen = ref(null) // Trackt, welches Submenü auf Mobile offen ist
function toggleMobileSubNav(routeKey) {
if (screenWidth.value < 1350) {
isMobileSubNavOpen.value = isMobileSubNavOpen.value === routeKey ? null : routeKey
}
}
// Funktionen zum Öffnen und Schließen der Subnavigation auf Desktop
function showSubNav(routeKey) {
isSubNavOpen.value = routeKey
}
function hideSubNav(routeKey) {
if (isSubNavOpen.value === routeKey) {
isSubNavOpen.value = null
}
}
const isMenuOpen = computed(() => mainStore.menuOpen)
const toggleMenu = () => mainStore.toggleMenu()
const toggleContactBubble = () => mainStore.toggleContactBubble()
const handleMobileClose = () => {
if (screenWidth.value < 1350 && isMenuOpen.value) {
toggleMenu()
} else {
hideSubNav()
}
}
const navigationLinks = [
{
routeKey: 'webagency',
label: 'webagency'
},
{
routeKey: '',
label: 'services',
subNav: [
{ routeKey: 'services-cms', label: 'menuCms' },
{ routeKey: 'services-seo', label: 'menuSEO' },
{ routeKey: 'services-accessibility', label: 'menuAccessibility' },
{ routeKey: 'services-ai', label: 'menuAi' },
]
},
{
routeKey: '',
label: 'sectors',
subNav: [
{ routeKey: 'sectors-schools', label: 'menuSchools' },
{ routeKey: 'sectors-film', label: 'menuFilm' }
]
},
{ routeKey: 'references', label: 'references' }
]
</script>
<style lang="sass">
.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
&.desk
.sub-nav
display: block
position: absolute
top: 3rem
right: 5%
background-color: rgba(white, .95)
padding: 2rem 2rem 1rem 2rem
text-align: left
cursor: pointer
border-bottom-left-radius: 1rem
border-bottom-right-radius: 1rem
list-style: none
.sub-nav-item
padding: 0 0 1.5rem 0
.sub-nav-link
position: relative
margin-left: 1.2rem
&::before
content: ''
width: .5rem
height: .4rem
background-color: rgba($primaryColor, .9)
border-radius: $loopShape
position: absolute
top: .4rem
left: -1.3rem
border-radius: 20px
&:hover
transform: scale(1.025)
&.mobile
top: 0
&.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
.submenu-toggle
background: none
border: none
padding: 0 0 0 0.5rem
margin-top: -1rem
cursor: pointer
display: inline-flex
align-items: center
color: white
transition: transform 0.3s ease
svg
transition: transform 0.3s ease
&.open svg,
svg.open
transform: rotate(180deg) // Pfeil zeigt nach oben, wenn offen
.sub-nav
overflow: hidden
max-height: 0
opacity: 0
transition: max-height 0.4s ease, opacity 0.4s ease
&.open
max-height: 500px
opacity: 1
.sub-nav
list-style: none
padding-left: 1.8rem
margin: 0
.sub-nav-item
padding-left: 1rem
margin-bottom: 0.2rem
.sub-nav-link
color: white
font-size: 1rem !important
line-height: 1.5rem
padding: 0.5rem 1.5rem
&:hover
&::before
content: ''
width: .5rem
height: .4rem
background-color: rgba($primaryColor, .9)
border-radius: $loopShape
position: absolute
top: 1rem
left: 0
border-radius: 20px
.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: .2rem
padding: 1rem 2.8rem .5rem 1.2rem
position: relative
font-size: 1.25rem !important
width: auto
max-width: 18rem
text-transform: uppercase
font-family: 'Mainfont-Bold'
&::before
content: ''
width: .8rem
height: .6rem
background-color: rgba($primaryColor, .9)
border-radius: $loopShape
position: absolute
top: 1.5rem
left: -.5rem
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: 98vh
border-radius: 5px
margin: 0
background-color: rgba($darkgrey, .97)
.closer
&::before, &::after
top: 2rem
right: 2rem
&::before
transform: rotate(45deg)
&::after
transform: rotate(-45deg)
nav
display: block
padding: 10vh 0
margin: 0 5vw
.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
&.desk
.sub-nav
top: 1.5rem
right: 2%
background-color: transparent
background-image: linear-gradient(to bottom, transparent 0%, white 15%)
</style>

View File

@ -120,7 +120,7 @@ export const i18nPages = {
tr: '/kosullar'
},
magazin: {
de: '/wissenswertes',
de: '/magazin',
en: '/magazine',
fr: '/magazine',
it: '/magazine',

View File

@ -5,7 +5,7 @@
"menu": {
"webagency": "Webagentur",
"menuAbout": "Über uns",
"menuMagazin": "Wissenswertes",
"menuMagazin": "Magazin",
"services": "Leistungen",
"sectors": "Branchen",
"menuCms": "Headless Content-Management-System (CMS)",

View File

@ -1,6 +1,6 @@
<template>
<div v-if="article" class="article">
<SideBarNaviSlider link="/wissenswertes">
<SideBarNaviSlider link="/magazin">
{{ $t('pages.article.artikelUebersicht') }}
</SideBarNaviSlider>

View File

@ -2,59 +2,77 @@
<div class="knowledgeBox topSpace">
<section class="teaserBox">
<div class="container">
<h1>Wissenswertes für digitale Entscheider</h1>
<h2>Webdesign und Webentwicklung, SEO, Performance & AI</h2>
<p>In unserem Fachmagazin erfahren Sie, wie moderne Webseiten aufgebaut sein müssen,
um technisch, inhaltlich und strategisch zu überzeugen.
Themen wie <b>KI-gestütztes SEO (AI-SEO)</b>, <b>nachhaltige Webentwicklung</b>, <b>Barrierefreiheit</b>,
<b>Ladezeit-Optimierung</b>, <b>User Experience</b> und <b>Headless CMS</b> zeigen, worauf es heute im Webdesign wirklich ankommt.</p>
<p>Entdecken Sie <b>praxisnahe Insights und zukunftsorientierte Lösungen</b> für mehr Sichtbarkeit, Effizienz und Erfolg im digitalen Raum.</p>
<h1>Digital Insights Magazin</h1>
<h2>Wissenswertes rund um moderne Weblösungen</h2>
<p>Hier finden Sie praxisnahe Beiträge zu Technik, Strategie und Gestaltung im Web. Wir bieten wertvolle Impulse und aktuelles Wissen,
damit Sie gut informiert sind und fundierte Entscheidungen für Ihre Webprojekte treffen können.
</p>
<div class="selectionZone">
<button
:class="{ active: isAllSelected }"
@click="selectAll"
>
Alle
</button>
<button
v-for="category in categories"
:key="category.id"
:class="{ active: selectedCategories.has(category.id) }"
@click="toggleCategory(category.id)"
>
{{ category.name }}
</button>
</div>
</div>
</section>
<section class="articleBox container">
<div class="grid">
<transition-group
name="article"
tag="div"
class="grid"
appear
>
<div
v-for="article in articles"
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>
<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>
<button class="btn mintBtn">{{ $t('pages.magazin.readmore') }}</button>
</div>
</NuxtLinkLocale>
</NuxtLinkLocale>
</div>
</div>
</transition-group>
</section>
</div>
</template>
<script setup lang="ts">
import { watch } from 'vue'
import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'
import { useLocalePath } from '#i18n'
const localePath = useLocalePath()
const mainStore = useMainStore()
const { articles } = storeToRefs(mainStore)
const { articles, categories } = storeToRefs(mainStore)
const truncateText = (text: string, length = 200) =>
text?.length > length ? text.substring(0, length) + '…' : text
@ -63,6 +81,29 @@ const currentDomain = typeof window !== 'undefined'
? window.location.origin
: 'https://www.digimedialoop.de'
const selectedCategories = ref<Set<number>>(new Set())
const isAllSelected = computed(() => selectedCategories.value.size === 0)
function toggleCategory(categoryId: number) {
if (selectedCategories.value.has(categoryId)) {
selectedCategories.value.delete(categoryId)
} else {
selectedCategories.value.add(categoryId)
}
}
function selectAll() {
selectedCategories.value.clear()
}
const filteredArticles = computed(() => {
if (isAllSelected.value) return articles.value
return articles.value.filter(article => {
if (!article.categories || article.categories.length === 0) return false
return article.categories.some(cat => selectedCategories.value.has(cat.id))
})
})
// SEO: JSON-LD für Artikelübersicht
watch(articles, (newVal) => {
@ -90,102 +131,150 @@ watch(articles, (newVal) => {
<style lang="sass">
.articleBox
display: flex
justify-content: center
width: 100%
.knowledgeBox
h1
margin: 2rem 0 0 0
h2
margin: 0 0 0 0
.selectionZone
border: 1px solid $lightgrey
border-radius: 1rem
padding: 1rem 1.5rem 0 1.5rem
background-color: lighten($beige, 5%)
margin: 0 0 4rem 0
button
all: unset
background-color: white
padding: .5rem 1.5rem
border-radius: .8rem
margin: 0 1rem 1rem 0
box-shadow: 1px 1px 1px 0 rgba(black, .3)
border: 1px solid darken($lightgrey, 8%)
transition: .5s
cursor: pointer
.grid
&:hover
transform: scale(1.05)
&.active
font-family: 'Mainfont-Bold'
box-shadow: 0 0 2px 0 rgba(black, .3)
background-color: darken($lightgrey, 10%)
border: 1px solid darken($lightgrey, 25%)
.articleBox
display: flex
justify-content: center
width: (9)0%
.grid
display: grid
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr))
gap: 2rem
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr))
gap: 2.5rem
justify-content: start
width: 100%
max-width: 100%
margin: 0 auto
transition: all 0.3s ease-in-out // weichere Umordnung der Items
.article
.article
width: 100%
max-width: 500px
border: 1px solid $beige
background: linear-gradient(to bottom right, white, $lightgrey)
border-radius: 1rem
transition: .5s
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)
transform: scale(1.05)
.article-link
position: relative
display: block
color: white
text-decoration: none
.article-enter-from,
.article-leave-to
opacity: 0
transform: scale(0.85)
.image-wrapper
position: relative
.article-enter-active,
.article-leave-active
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: 280px
border: 1px solid $lightgrey
height: 500px
object-fit: cover
border-top-left-radius: 1rem
border-top-right-radius: 1rem
opacity: .6
.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
button
position: absolute
bottom: .6rem
right: 0rem
border: 1px solid darken($primaryColor, 30%)
.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
box-shadow: 1px 1px 4px 2px rgba(black, .2)
background-color: darken($primaryColor, 15%)
letter-spacing: .05rem
line-height: 140%
font-family: 'Mainfont-Bold'
margin: .2rem 1rem
//text-transform: uppercase
hyphens: auto
.overlay
position: absolute
top: 0
left: 0
width: 80%
height: auto
min-height: 60%
background-image: linear-gradient(to bottom right, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.96) )
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: 1rem
//border-radius: .5rem
text-align: left
transition: .3s
box-shadow: 2px 2px 5px 3px rgba(black, .2)
h2
color: black
font-size: 1.1rem
line-height: 140%
font-family: 'Mainfont-Bold'
margin: .2rem 1rem
.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
.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>

View File

@ -144,7 +144,7 @@ const mainStore = useMainStore();
const { projects, companyinfo } = storeToRefs(mainStore)
const navigateToArticle = () => {
router.push('/wissenswertes/artikel/design-und-inhalt-sauber-getrennt-warum-headless-webdesign-die-beste-wahl-fuer-moderne-unternehmen-ist');
router.push('/artikel/design-und-inhalt-sauber-getrennt-warum-headless-webdesign-die-beste-wahl-fuer-moderne-unternehmen-ist');
};
const projectItems = computed(() => {

View File

@ -93,6 +93,10 @@ interface NewsArticle {
name: string
// optional: weitere Felder aus dem Team-Modell
} | null
categories?: {
id: number
name: string
}[]
}
interface ContactData {
@ -118,6 +122,7 @@ export const useMainStore = defineStore('main', {
customers: [] as Customer[],
projects: [] as CustomerProject[],
articles: [] as NewsArticle[],
categories: [] as { id: number; name: string }[],
dataFetched: false,
loading: false,
error: null as { message: string; stack?: string } | null,
@ -211,7 +216,7 @@ export const useMainStore = defineStore('main', {
const { public: cfg } = useRuntimeConfig()
try {
const [companyRes, pagesRes, customersRes, projectsRes, articlesRes] = await Promise.all([
const [companyRes, pagesRes, customersRes, projectsRes, articlesRes, categoriesRes] = await Promise.all([
$fetch(`${cfg.cmsBaseUrl}/api/companyinfo?populate=*`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
@ -224,7 +229,10 @@ export const useMainStore = defineStore('main', {
$fetch(`${cfg.cmsBaseUrl}/api/references?populate=projectImages,Technologien,customer&sort=launchDate:desc`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
$fetch(`${cfg.cmsBaseUrl}/api/newsarticels?populate=image,SEO,author&locale=all&sort=createdAt:desc`, {
$fetch(`${cfg.cmsBaseUrl}/api/newsarticels?populate=image,SEO,categories,author&locale=all&sort=createdAt:desc`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
$fetch(`${cfg.cmsBaseUrl}/api/magazin-cats?populate=*&locale=all`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
])
@ -348,9 +356,18 @@ export const useMainStore = defineStore('main', {
name: a.author.data.attributes.name,
}
: null,
categories: a.categories?.data?.map((c: any) => ({
id: c.id,
name: c.attributes.category,
})) ?? [],
}
})
this.categories = categoriesRes.data.map((item: any) => ({
id: item.id,
name: item.attributes.category,
}))
this.dataFetched = true
} catch (err) {
const e = err as Error