schon/storefront/components/base/header/catalog.vue
Alexandr SaVBaD Waltz 129ad1a6fa Features: 1) Build standalone pages for search, contact, catalog, category, brand, product, and home with localized metadata and scoped styles; 2) Add extensive TypeScript definitions for API and app-level structures, including products, orders, brands, and categories; 3) Implement i18n configuration with dynamic browser language detection and fallback system;
Fixes: None;

Extra: 1) Create Pinia stores for app, user, category, and company management; 2) Add utility functions for error handling and category slug lookups; 3) Include German locale file and robots.txt for improved SEO and accessibility; 4) Add SVG assets and improve general folder structure for better maintainability.
2025-06-27 00:10:35 +03:00

272 lines
No EOL
5.7 KiB
Vue

<template>
<div class="catalog" ref="blockRef">
<button
@click="setBlock(!isBlockOpen)"
class="catalog__button"
:class="[{ active: isBlockOpen }]"
>
{{ t('header.catalog.title') }}
<span></span>
</button>
<div class="container">
<div class="categories" :class="[{active: isBlockOpen}]">
<div class="categories__block" v-if="categories.length > 0">
<div class="categories__left">
<p
v-for="category in categories"
:key="category.node.uuid"
:class="[{ active: category.node.uuid === activeCategory.uuid }]"
@click="setActiveCategory( category.node)"
>
{{ category.node.name }}
</p>
</div>
<div class="categories__main">
<div
class="categories__main-block"
v-for="mainChildren in activeCategory.children"
:key="mainChildren.uuid"
>
<nuxt-link-locale
:to="`/catalog/${mainChildren.slug}`"
class="categories__main-link"
@click="setBlock(false)"
>
{{ mainChildren.name }}
</nuxt-link-locale>
<div class="categories__main-list">
<nuxt-link-locale
v-for="children in mainChildren.children"
:key="children.uuid"
:to="`/catalog/${children.slug}`"
@click="setBlock(false)"
>
{{ children.name }}
</nuxt-link-locale >
</div>
</div>
</div>
</div>
<div class="categories__empty" v-else><p>{{ t('header.catalog.empty') }}</p></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {ref} from "vue";
import {onClickOutside} from "@vueuse/core";
import type {ICategory} from "~/types";
import {useCategoryStore} from "~/stores/category";
const { t } = useI18n()
const categoryStore = useCategoryStore();
const categories = computed(() => categoryStore.categories)
const isBlockOpen = ref<boolean>(false)
const setBlock = (state: boolean) => {
isBlockOpen.value = state
}
// TODO: add loading state
// TODO: fix displaying main part (children categories)
const blockRef = ref(null)
onClickOutside(blockRef, () => setBlock(false))
const activeCategory = ref<ICategory>(categories.value[0]?.node)
const setActiveCategory = (category: ICategory) => {
activeCategory.value = category
}
</script>
<style lang="scss" scoped>
.catalog {
&__button {
cursor: pointer;
border-radius: $default_border_radius;
background-color: rgba($accent, 0.2);
border: 1px solid $accent;
padding: 5px 20px;
display: flex;
align-items: center;
gap: 10px;
transition: 0.2s;
color: $accent;
font-size: 16px;
font-weight: 600;
@include hover {
background-color: $accent;
color: $white;
}
& span {
transition: 0.2s;
font-size: 26px;
}
&.active {
background-color: $accent;
color: $white;
& span {
transform: rotate(-180deg);
}
}
}
}
.container {
position: absolute;
left: 50%;
top: 110%;
transform: translateX(-50%);
width: 100%;
}
.categories {
border-radius: $default_border_radius;
width: 100%;
background-color: $white;
box-shadow: 0 0 15px 1px $accentLight;
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.2s ease;
overflow: hidden;
&.active {
grid-template-rows: 1fr;
}
& > * {
min-height: 0;
}
&__block {
display: grid;
grid-template-columns: 20% 80%;
max-height: 60vh;
}
&__columns {
& div {
padding: 20px 50px;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-column-gap: 30px;
grid-row-gap: 5px;
& p {
cursor: pointer;
padding: 5px 20px;
transition: 0.2s;
font-size: 16px;
font-weight: 600;
@include hover {
color: $accent;
}
}
}
}
&__left {
flex-shrink: 0;
padding-block: 10px;
overflow: auto;
& p {
cursor: pointer;
transition: 0.2s;
flex-shrink: 0;
padding: 10px;
border-left: 3px solid $white;
font-weight: 700;
@include hover {
color: $accent;
}
&.active {
border-color: $accent;
color: $accent;
}
}
}
&__main {
padding: 20px;
border-left: 2px solid $accentDark;
overflow: auto;
&-block {
padding-bottom: 15px;
border-bottom: 1px solid #eeeeee;
margin-bottom: 15px;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
}
&-link {
position: relative;
width: fit-content;
cursor: pointer;
font-weight: 600;
font-size: 16px;
&::after {
content: "";
position: absolute;
bottom: -2px;
left: 0;
height: 2px;
width: 0;
transition: all .3s ease;
background-color: $accent;
}
@include hover {
&::after {
width: 100%;
}
}
}
&-list {
margin-top: 10px;
display: grid;
grid-template-columns: repeat(3,1fr);
grid-column-gap: 30px;
grid-row-gap: 5px;
& a {
cursor: pointer;
transition: 0.1s;
font-size: 14px;
@include hover {
color: $accent;
}
}
}
}
&__empty {
& p {
padding: 20px;
text-align: center;
font-size: 16px;
font-weight: 600;
}
}
}
</style>