Compare commits
No commits in common. "storefront-nuxt" and "master" have entirely different histories.
storefront
...
master
309 changed files with 14 additions and 31326 deletions
|
|
@ -12,7 +12,6 @@ coverage.*
|
||||||
*.py,cover
|
*.py,cover
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
desktop.ini
|
desktop.ini
|
||||||
tmp/
|
|
||||||
|
|
||||||
# Cache directories
|
# Cache directories
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
@ -52,8 +51,6 @@ wheels/
|
||||||
share/python-wheels/
|
share/python-wheels/
|
||||||
pip-log.txt
|
pip-log.txt
|
||||||
pip-delete-this-directory.txt
|
pip-delete-this-directory.txt
|
||||||
storefront/node_modules/
|
|
||||||
storefront/.nuxt
|
|
||||||
|
|
||||||
# Git
|
# Git
|
||||||
.git/
|
.git/
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
# syntax=docker/dockerfile:1
|
|
||||||
FROM node:24-bookworm-slim AS build
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ARG SCHON_BASE_DOMAIN
|
|
||||||
ARG SCHON_PROJECT_NAME
|
|
||||||
ARG SCHON_LANGUAGE_CODE
|
|
||||||
ENV SCHON_BASE_DOMAIN=$SCHON_BASE_DOMAIN
|
|
||||||
ENV SCHON_PROJECT_NAME=$SCHON_PROJECT_NAME
|
|
||||||
ENV SCHON_LANGUAGE_CODE=$SCHON_LANGUAGE_CODE
|
|
||||||
|
|
||||||
COPY ./storefront/package.json ./storefront/package-lock.json ./
|
|
||||||
RUN npm ci --include=optional
|
|
||||||
|
|
||||||
COPY ./storefront ./
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
FROM node:22-bookworm-slim AS runtime
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV HOST=0.0.0.0
|
|
||||||
ENV PORT=3000
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodeapp \
|
|
||||||
&& adduser --system --uid 1001 --ingroup nodeapp --home /home/nodeapp nodeapp
|
|
||||||
USER nodeapp
|
|
||||||
|
|
||||||
COPY --from=build /app/.output/ ./
|
|
||||||
|
|
||||||
RUN install -d -m 0755 -o nodeapp -g nodeapp /home/nodeapp \
|
|
||||||
&& printf '#!/bin/sh\nif [ \"$DEBUG\" = \"1\" ]; then export NODE_ENV=development; else export NODE_ENV=production; fi\nexec node /app/server/index.mjs\n' > /home/nodeapp/start.sh \
|
|
||||||
&& chown nodeapp:nodeapp /home/nodeapp/start.sh \
|
|
||||||
&& chmod +x /home/nodeapp/start.sh
|
|
||||||
|
|
||||||
USER nodeapp
|
|
||||||
CMD ["sh", "/home/nodeapp/start.sh"]
|
|
||||||
|
|
@ -126,7 +126,7 @@ services:
|
||||||
container_name: worker
|
container_name: worker
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./Dockerfiles/worker.Dockerfile
|
dockerfile: Dockerfiles/worker.Dockerfile
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
|
|
@ -154,7 +154,7 @@ services:
|
||||||
container_name: stock_updater
|
container_name: stock_updater
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./Dockerfiles/stock_updater.Dockerfile
|
dockerfile: Dockerfiles/stock_updater.Dockerfile
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
|
|
@ -182,7 +182,7 @@ services:
|
||||||
container_name: beat
|
container_name: beat
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./Dockerfiles/beat.Dockerfile
|
dockerfile: Dockerfiles/beat.Dockerfile
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
|
|
@ -214,30 +214,6 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "9090:9090"
|
- "9090:9090"
|
||||||
|
|
||||||
storefront:
|
|
||||||
container_name: storefront
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./Dockerfiles/storefront.Dockerfile
|
|
||||||
args:
|
|
||||||
- DEBUG=${DEBUG}
|
|
||||||
- SCHON_BASE_DOMAIN=${SCHON_BASE_DOMAIN}
|
|
||||||
- SCHON_PROJECT_NAME=${SCHON_PROJECT_NAME}
|
|
||||||
- SCHON_LANGUAGE_CODE=${SCHON_LANGUAGE_CODE}
|
|
||||||
restart: always
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- NUXT_HOST=0.0.0.0
|
|
||||||
- NUXT_PORT=3000
|
|
||||||
- NUXT_DEVTOOLS_ENABLED=${DEBUG}
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
depends_on:
|
|
||||||
app:
|
|
||||||
condition: service_started
|
|
||||||
logging: *default-logging
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
|
|
||||||
|
|
@ -619,8 +619,6 @@ class CreateAddress(Mutation):
|
||||||
raw_data = String(
|
raw_data = String(
|
||||||
required=True, description=_("original address string provided by the user")
|
required=True, description=_("original address string provided by the user")
|
||||||
)
|
)
|
||||||
address_line_1 = String(required=True)
|
|
||||||
address_line_2 = String(required=False)
|
|
||||||
|
|
||||||
address = Field(AddressType)
|
address = Field(AddressType)
|
||||||
|
|
||||||
|
|
|
||||||
11
engine/core/static/robots_frontend.txt
Normal file
11
engine/core/static/robots_frontend.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /*/public/wishlist/
|
||||||
|
Disallow: /*/public/cart/
|
||||||
|
Disallow: /*/public/checkout/
|
||||||
|
Disallow: /*/auth/sign-in/
|
||||||
|
Disallow: /*/auth/sign-up/
|
||||||
|
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://schon.wiseless.xyz/sitemap.xml
|
||||||
|
Host: schon.wiseless.xyz
|
||||||
24
storefront/.gitignore
vendored
24
storefront/.gitignore
vendored
|
|
@ -1,24 +0,0 @@
|
||||||
# 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
|
|
||||||
0
storefront/.gitkeep
Normal file
0
storefront/.gitkeep
Normal file
19
storefront/app.config.d.ts
vendored
19
storefront/app.config.d.ts
vendored
|
|
@ -1,19 +0,0 @@
|
||||||
declare module 'nuxt/schema' {
|
|
||||||
interface AppConfig {
|
|
||||||
i18n: {
|
|
||||||
supportedLocales: Array<{
|
|
||||||
code: string;
|
|
||||||
file: string;
|
|
||||||
default: boolean;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
ui: {
|
|
||||||
showBreadcrumbs: boolean;
|
|
||||||
showSearchBar: boolean;
|
|
||||||
isHeaderFixed: boolean;
|
|
||||||
isAuthModals: boolean;
|
|
||||||
notificationPosition: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export {};
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { SUPPORTED_LOCALES } from '@appConstants';
|
|
||||||
|
|
||||||
export default defineAppConfig({
|
|
||||||
i18n: {
|
|
||||||
supportedLocales: SUPPORTED_LOCALES,
|
|
||||||
},
|
|
||||||
ui: {
|
|
||||||
showBreadcrumbs: true,
|
|
||||||
showSearchBar: true,
|
|
||||||
isHeaderFixed: true,
|
|
||||||
isAuthModals: false,
|
|
||||||
notificationPosition: 'top-right',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="main"
|
|
||||||
:style="{ 'padding-top': uiConfig.isHeaderFixed ? '84px': '0' }"
|
|
||||||
>
|
|
||||||
<nuxt-loading-indicator :color="primaryColor" />
|
|
||||||
<base-header />
|
|
||||||
<ui-breadcrumbs v-if="uiConfig.showBreadcrumbs && showBreadcrumbs" />
|
|
||||||
<transition
|
|
||||||
name="opacity"
|
|
||||||
mode="out-in"
|
|
||||||
v-if="uiConfig.isAuthModals"
|
|
||||||
>
|
|
||||||
<base-auth v-if="activeState">
|
|
||||||
<forms-login v-if="appStore.isLogin" />
|
|
||||||
<forms-register v-if="appStore.isRegister" />
|
|
||||||
<forms-reset-password v-if="appStore.isForgot" />
|
|
||||||
<forms-new-password v-if="appStore.isReset" />
|
|
||||||
</base-auth>
|
|
||||||
</transition>
|
|
||||||
<nuxt-page />
|
|
||||||
<ui-button
|
|
||||||
:type="'button'"
|
|
||||||
class="demo__button"
|
|
||||||
v-if="isDemoMode"
|
|
||||||
@click="appStore.setDemoSettings(!appStore.isDemoSettings)"
|
|
||||||
>
|
|
||||||
<icon
|
|
||||||
name="material-symbols:settings"
|
|
||||||
size="30"
|
|
||||||
/>
|
|
||||||
</ui-button>
|
|
||||||
<transition name="opacity" mode="out-in">
|
|
||||||
<demo-settings />
|
|
||||||
</transition>
|
|
||||||
<base-footer />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useRefresh} from '@composables/auth';
|
|
||||||
import {useLanguages, useLocaleRedirect} from '@composables/languages';
|
|
||||||
import {useCompanyInfo} from '@composables/company';
|
|
||||||
import {useCategories} from '@composables/categories';
|
|
||||||
import {useProjectConfig} from '@composables/config';
|
|
||||||
|
|
||||||
const { locale } = useI18n();
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const { $appHelpers } = useNuxtApp();
|
|
||||||
|
|
||||||
const { isDemoMode, uiConfig } = useProjectConfig();
|
|
||||||
const switchLocalePath = useSwitchLocalePath();
|
|
||||||
|
|
||||||
const showBreadcrumbs = computed(() => {
|
|
||||||
const name = typeof route.name === 'string' ? route.name : '';
|
|
||||||
return ![
|
|
||||||
'index',
|
|
||||||
'search',
|
|
||||||
'profile',
|
|
||||||
'activate-user',
|
|
||||||
'reset-password',
|
|
||||||
'auth-sign-in',
|
|
||||||
'auth-sign-up',
|
|
||||||
'auth-reset-password',
|
|
||||||
'contact',
|
|
||||||
'blog',
|
|
||||||
'docs'
|
|
||||||
].some(prefix => name.startsWith(prefix));
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeState = computed(() => appStore.activeAuthState);
|
|
||||||
|
|
||||||
const cookieLocale = useCookie(
|
|
||||||
$appHelpers.COOKIES_LOCALE_KEY,
|
|
||||||
{
|
|
||||||
default: () => $appHelpers.DEFAULT_LOCALE,
|
|
||||||
path: '/'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { refresh } = useRefresh();
|
|
||||||
const { getCategories } = await useCategories();
|
|
||||||
const { isSupportedLocale } = useLocaleRedirect();
|
|
||||||
|
|
||||||
let refreshInterval: NodeJS.Timeout;
|
|
||||||
|
|
||||||
if (import.meta.server) {
|
|
||||||
await Promise.all([
|
|
||||||
refresh(),
|
|
||||||
useLanguages(),
|
|
||||||
useCompanyInfo(),
|
|
||||||
getCategories()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [appStore.activeAuthState, appStore.isDemoSettings],
|
|
||||||
([authState, isDemo]) => {
|
|
||||||
appStore.setOverflowHidden(authState !== '' || isDemo);
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(locale, () => {
|
|
||||||
useHead({
|
|
||||||
htmlAttrs: {
|
|
||||||
lang: locale.value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let stopWatcher: VoidFunction = () => {};
|
|
||||||
|
|
||||||
if (!cookieLocale.value) {
|
|
||||||
cookieLocale.value = $appHelpers.DEFAULT_LOCALE;
|
|
||||||
await router.push({path: switchLocalePath(cookieLocale.value)});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locale.value !== cookieLocale.value) {
|
|
||||||
if (isSupportedLocale(cookieLocale.value)) {
|
|
||||||
await router.push({
|
|
||||||
path: switchLocalePath(cookieLocale.value),
|
|
||||||
query: route.query
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cookieLocale.value = $appHelpers.DEFAULT_LOCALE;
|
|
||||||
|
|
||||||
await router.push({
|
|
||||||
path: switchLocalePath($appHelpers.DEFAULT_LOCALE),
|
|
||||||
query: route.query
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const primaryColor = ref('');
|
|
||||||
|
|
||||||
const getCssVariable = (name: string): string => {
|
|
||||||
if (import.meta.client) {
|
|
||||||
return getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue(name)
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted( async () => {
|
|
||||||
refreshInterval = setInterval(async () => {
|
|
||||||
await refresh();
|
|
||||||
}, 600000);
|
|
||||||
|
|
||||||
stopWatcher = watch(
|
|
||||||
() => appStore.isOverflowHidden,
|
|
||||||
(hidden) => {
|
|
||||||
const root = document.documentElement;
|
|
||||||
const body = document.body;
|
|
||||||
if (hidden) {
|
|
||||||
root.classList.add('lock-scroll');
|
|
||||||
body.classList.add('lock-scroll');
|
|
||||||
} else {
|
|
||||||
root.classList.remove('lock-scroll');
|
|
||||||
body.classList.remove('lock-scroll');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
primaryColor.value = getCssVariable('--primary');
|
|
||||||
|
|
||||||
useHead({
|
|
||||||
htmlAttrs: {
|
|
||||||
lang: locale.value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
stopWatcher()
|
|
||||||
document.documentElement.classList.remove('lock-scroll');
|
|
||||||
document.body.classList.remove('lock-scroll');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.main {
|
|
||||||
background-color: $main_hover;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lock-scroll {
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo {
|
|
||||||
&__button {
|
|
||||||
position: fixed !important;
|
|
||||||
width: fit-content !important;
|
|
||||||
bottom: 20px;
|
|
||||||
right: 20px;
|
|
||||||
padding: 10px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,108 +0,0 @@
|
||||||
/* ===== SOURCE CODE PRO ===== */
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Code Pro';
|
|
||||||
src: url('../../fonts/SourceCodePro/SourceCodePro-Black.ttf');
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Code Pro';
|
|
||||||
src: url('../../fonts/SourceCodePro/SourceCodePro-ExtraBold.ttf');
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Code Pro';
|
|
||||||
src: url('../../fonts/SourceCodePro/SourceCodePro-Bold.ttf');
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Code Pro';
|
|
||||||
src: url('../../fonts/SourceCodePro/SourceCodePro-SemiBold.ttf');
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Code Pro';
|
|
||||||
src: url('../../fonts/SourceCodePro/SourceCodePro-Medium.ttf');
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Code Pro';
|
|
||||||
src: url('../../fonts/SourceCodePro/SourceCodePro-Regular.ttf');
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Code Pro';
|
|
||||||
src: url('../../fonts/SourceCodePro/SourceCodePro-Light.ttf');
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Source Code Pro';
|
|
||||||
src: url('../../fonts/SourceCodePro/SourceCodePro-ExtraLight.ttf');
|
|
||||||
font-weight: 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== PLAYFAIR DISPLAY ===== */
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Playfair Display';
|
|
||||||
src: url('../../fonts/PlayfairDisplay/PlayfairDisplay-Black.ttf');
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Playfair Display';
|
|
||||||
src: url('../../fonts/PlayfairDisplay/PlayfairDisplay-ExtraBold.ttf');
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Playfair Display';
|
|
||||||
src: url('../../fonts/PlayfairDisplay/PlayfairDisplay-Bold.ttf');
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Playfair Display';
|
|
||||||
src: url('../../fonts/PlayfairDisplay/PlayfairDisplay-SemiBold.ttf');
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Playfair Display';
|
|
||||||
src: url('../../fonts/PlayfairDisplay/PlayfairDisplay-Medium.ttf');
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Playfair Display';
|
|
||||||
src: url('../../fonts/PlayfairDisplay/PlayfairDisplay-Regular.ttf');
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== INTER ===== */
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
src: url('../../fonts/Inter/Inter-Black.ttf');
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
src: url('../../fonts/Inter/Inter-ExtraBold.ttf');
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
src: url('../../fonts/Inter/Inter-Bold.ttf');
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
src: url('../../fonts/Inter/Inter-SemiBold.ttf');
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
src: url('../../fonts/Inter/Inter-Medium.ttf');
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
src: url('../../fonts/Inter/Inter-Regular.ttf');
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
@mixin hover {
|
|
||||||
@media (hover: hover) and (pointer: fine) {
|
|
||||||
&:hover {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
$font_default: 'Inter', sans-serif;
|
|
||||||
|
|
||||||
$default_border_radius: 8px;
|
|
||||||
$less_border_radius: 4px;
|
|
||||||
|
|
||||||
$main: var(--main);
|
|
||||||
$main_hover: var(--main_hover);
|
|
||||||
$title_bg: var(--title_bg);
|
|
||||||
$primary: var(--primary);
|
|
||||||
$primary_hover: var(--primary_hover);
|
|
||||||
$primary_dark: var(--primary_dark);
|
|
||||||
$primary_gradient: var(--primary_gradient);
|
|
||||||
$primary_shadow: var(--primary_shadow);
|
|
||||||
$primary_shadow_hover: var(--primary_shadow_hover);
|
|
||||||
$blackout: var(--blackout);
|
|
||||||
|
|
||||||
$secondary: var(--secondary);
|
|
||||||
$secondary_hover: var(--secondary_hover);
|
|
||||||
|
|
||||||
$disabled: var(--disabled);
|
|
||||||
$disabled_secondary: var(--disabled_secondary);
|
|
||||||
$border: var(--border);
|
|
||||||
$border_hover: var(--border_hover);
|
|
||||||
|
|
||||||
$text: var(--text);
|
|
||||||
$link_primary: var(--link_primary);
|
|
||||||
$link_primary_hover: var(--link_primary_hover);
|
|
||||||
$link_secondary: var(--link_secondary);
|
|
||||||
$link_secondary_hover: var(--link_secondary_hover);
|
|
||||||
$skeleton: var(--skeleton);
|
|
||||||
|
|
||||||
$rating: var(--rating);
|
|
||||||
$success: var(--success);
|
|
||||||
$error: var(--error);
|
|
||||||
$info: var(--info);
|
|
||||||
$warning: var(--warning);
|
|
||||||
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--main: #fff;
|
|
||||||
--main_hover: #f9fafb;
|
|
||||||
--title_bg: #f8f8f8;
|
|
||||||
--primary: #111827;
|
|
||||||
--primary_hover: #242c38;
|
|
||||||
--primary_dark: #1a1a1a;
|
|
||||||
--primary_gradient: linear-gradient(to bottom, rgba(26, 26, 26, 1) 0%,rgba(31, 41, 55, 1) 100%);
|
|
||||||
--primary_shadow: rgba(0, 0, 0, 0.1);
|
|
||||||
--primary_shadow_hover: rgba(0, 0, 0, 0.3);
|
|
||||||
--blackout: rgba(0, 0, 0, 0.4);
|
|
||||||
|
|
||||||
--secondary: #374151;
|
|
||||||
--secondary_hover: #29323f;
|
|
||||||
|
|
||||||
--disabled: #0a0f1a;
|
|
||||||
--disabled_secondary: #9a9a9a;
|
|
||||||
--border: #e5e7eb;
|
|
||||||
--border_hover: #505052;
|
|
||||||
|
|
||||||
--text: #4b5563;
|
|
||||||
--link_primary: #1f2937;
|
|
||||||
--link_primary_hover: #29323f;
|
|
||||||
--link_secondary: #c2c7ce;
|
|
||||||
--link_secondary_hover: #a4a8ad;
|
|
||||||
--skeleton: rgba(255, 255, 255, 0.61);
|
|
||||||
|
|
||||||
--rating: #facc15;
|
|
||||||
--success: #67c23a;
|
|
||||||
--error: #f56c6c;
|
|
||||||
--info: #909399;
|
|
||||||
--warning: #e6a23c;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme='dark'] {
|
|
||||||
--main: #1a1a1a;
|
|
||||||
--main_hover: #242424;
|
|
||||||
--title_bg: #111111;
|
|
||||||
--primary: #f3f4f6;
|
|
||||||
--primary_hover: #ffffff;
|
|
||||||
--primary_dark: #e5e7eb;
|
|
||||||
--primary_gradient: linear-gradient(to bottom, rgba(243, 244, 246, 1) 0%, rgba(229, 231, 235, 1) 100%);
|
|
||||||
--primary_shadow: rgba(0, 0, 0, 0.5);
|
|
||||||
--primary_shadow_hover: rgba(0, 0, 0, 0.7);
|
|
||||||
--blackout: rgba(0, 0, 0, 0.5);
|
|
||||||
|
|
||||||
--secondary: #9ca3af;
|
|
||||||
--secondary_hover: #d1d5db;
|
|
||||||
|
|
||||||
--disabled: #4b5563;
|
|
||||||
--disabled_secondary: #6b7280;
|
|
||||||
--border: #374151;
|
|
||||||
--border_hover: #4b5563;
|
|
||||||
|
|
||||||
--text: #e5e7eb;
|
|
||||||
--link_primary: #aba7a7;
|
|
||||||
--link_primary_hover: #858282;
|
|
||||||
--link_secondary: #9ca3af;
|
|
||||||
--link_secondary_hover: #cbd5e1;
|
|
||||||
--skeleton: rgba(255, 255, 255, 0.1);
|
|
||||||
|
|
||||||
--rating: #facc15;
|
|
||||||
--success: #67c23a;
|
|
||||||
--error: #f56c6c;
|
|
||||||
--info: #909399;
|
|
||||||
--warning: #e6a23c;
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
@use "modules/normalize";
|
|
||||||
@use "modules/transitions";
|
|
||||||
@use "global/mixins";
|
|
||||||
@use "global/variables";
|
|
||||||
|
|
||||||
// UI
|
|
||||||
@use "ui/badge";
|
|
||||||
@use "ui/collapse";
|
|
||||||
@use "ui/notification";
|
|
||||||
@use "ui/rating";
|
|
||||||
@use "ui/select";
|
|
||||||
@use "ui/slider";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
@use "../global/variables" as *;
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
overflow-x: hidden;
|
|
||||||
font-family: $font_default;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
overflow-x: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
input, textarea, button {
|
|
||||||
font-family: $font_default;
|
|
||||||
outline: none;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1500px;
|
|
||||||
margin-inline: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: $primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: $main_hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
scrollbar-color: $primary $main_hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--el-color-primary: #{$primary} !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-skeleton__item {
|
|
||||||
--el-skeleton-color: #c9ccd0 !important;
|
|
||||||
--el-skeleton-to-color: #c3c3c7 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-badge__content {
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1680px) {
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1300px) {
|
|
||||||
.container {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
.opacity-enter-active,
|
|
||||||
.opacity-leave-active {
|
|
||||||
transition: 0.3s ease all;
|
|
||||||
}
|
|
||||||
.opacity-enter-from,
|
|
||||||
.opacity-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fromTop-enter-active,
|
|
||||||
.fromTop-leave-active {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.fromTop-enter-from,
|
|
||||||
.fromTop-leave-to {
|
|
||||||
transform: translateY(-3rem);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fromLeft-enter-active,
|
|
||||||
.fromLeft-leave-active {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.fromLeft-enter-from,
|
|
||||||
.fromLeft-leave-to {
|
|
||||||
transform: translateX(-3rem);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
@use "../global/variables" as *;
|
|
||||||
|
|
||||||
.el-badge__content {
|
|
||||||
background-color: $disabled !important;
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
@use "../global/variables" as *;
|
|
||||||
|
|
||||||
.el-collapse {
|
|
||||||
border: none !important;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
padding-block: 20px
|
|
||||||
}
|
|
||||||
.el-collapse-item {
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
border: 1px solid $border;
|
|
||||||
}
|
|
||||||
.el-collapse-item__header {
|
|
||||||
background-color: transparent !important;
|
|
||||||
border-bottom: none !important;
|
|
||||||
line-height: 100% !important;
|
|
||||||
font-size: 14px !important;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
padding-inline: 8px !important;
|
|
||||||
color: $primary_dark !important;
|
|
||||||
}
|
|
||||||
.el-collapse-item__header.focusing:focus:not(:hover) {
|
|
||||||
color: $primary_dark !important;
|
|
||||||
}
|
|
||||||
.el-collapse-item__wrap {
|
|
||||||
border-top: 2px solid $border;
|
|
||||||
border-bottom: none !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
.el-collapse-item__content {
|
|
||||||
padding: 10px !important;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
.el-collapse .el-icon {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
@use "../global/variables" as *;
|
|
||||||
|
|
||||||
.el-notification {
|
|
||||||
border: 2px solid $primary !important;
|
|
||||||
transition: all 0.3s ease !important;
|
|
||||||
|
|
||||||
&__progress {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 3px;
|
|
||||||
background-color: $primary_dark;
|
|
||||||
animation: progress-animation linear forwards;
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
background-color: $success;
|
|
||||||
}
|
|
||||||
&.error {
|
|
||||||
background-color: $error;
|
|
||||||
}
|
|
||||||
&.info {
|
|
||||||
background-color: $info;
|
|
||||||
}
|
|
||||||
&.warning {
|
|
||||||
background-color: $warning;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
background-color: $primary;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
color: $main;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-notification__closeBtn {
|
|
||||||
color: $primary !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes progress-animation {
|
|
||||||
0% {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
@use "../global/variables" as *;
|
|
||||||
|
|
||||||
.el-rate {
|
|
||||||
height: unset !important;
|
|
||||||
}
|
|
||||||
.el-rate .el-rate__icon.is-active {
|
|
||||||
color: $rating !important;
|
|
||||||
}
|
|
||||||
.el-rate .el-rate__icon {
|
|
||||||
color: $rating !important;
|
|
||||||
font-size: 16px !important;
|
|
||||||
margin-right: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
@use "../global/variables" as *;
|
|
||||||
|
|
||||||
.el-select__wrapper {
|
|
||||||
height: 36px !important;
|
|
||||||
min-height: 36px !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
.el-slider {
|
|
||||||
width: 90% !important;
|
|
||||||
margin-inline: auto;
|
|
||||||
height: 30px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-slider__bar, .el-slider__runway {
|
|
||||||
height: 4px !important;
|
|
||||||
}
|
|
||||||
.el-slider__button-wrapper {
|
|
||||||
width: 34px !important;
|
|
||||||
height: 34px !important;
|
|
||||||
}
|
|
||||||
.el-slider__button {
|
|
||||||
width: 16px !important;
|
|
||||||
height: 16px !important;
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="auth">
|
|
||||||
<div class="auth__content" ref="modalRef">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {onClickOutside} from '@vueuse/core';
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
appStore.unsetActiveAuthState();
|
|
||||||
};
|
|
||||||
|
|
||||||
const modalRef = ref(null);
|
|
||||||
onClickOutside(modalRef, () => closeModal());
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.auth {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 3;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 2;
|
|
||||||
top: 125px;
|
|
||||||
background-color: $main;
|
|
||||||
width: 600px;
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
.auth {
|
|
||||||
&__content {
|
|
||||||
width: 85%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
.auth {
|
|
||||||
&__content {
|
|
||||||
padding: 20px 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
<template>
|
|
||||||
<footer class="footer">
|
|
||||||
<div class="container">
|
|
||||||
<div class="footer__wrapper">
|
|
||||||
<div class="footer__main">
|
|
||||||
<div class="footer__left">
|
|
||||||
<nuxt-link-locale to="/" class="footer__logo">
|
|
||||||
SCHON
|
|
||||||
</nuxt-link-locale>
|
|
||||||
<p class="footer__text">{{ t('footer.text') }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="footer__columns">
|
|
||||||
<div class="footer__column">
|
|
||||||
<h6>{{ t('footer.shop') }}</h6>
|
|
||||||
<nuxt-link-locale to="/shop">{{ t('footer.allProducts') }}</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale to="/catalog">{{ t('footer.catalog') }}</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale to="/brands">{{ t('footer.brands') }}</nuxt-link-locale>
|
|
||||||
</div>
|
|
||||||
<div class="footer__column">
|
|
||||||
<h6>{{ t('footer.help') }}</h6>
|
|
||||||
<nuxt-link-locale to="/contact">{{ t('contact.title') }}</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale to="/docs/privacy-policy">{{ t('docs.policy.title') }}</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale to="/docs/terms-and-conditions">{{ t('docs.terms.title') }}</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale to="/docs/shipping-information">{{ t('docs.shipping.title') }}</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale to="/docs/return-policy">{{ t('docs.return.title') }}</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale to="/docs/about-us">{{ t('docs.about.title') }}</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale to="/docs/faq">{{ t('docs.faq.title') }}</nuxt-link-locale>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="footer__bottom">
|
|
||||||
<p>© {{ actualYear }} Schon. {{ t('footer.rights') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const companyStore = useCompanyStore();
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const companyInfo = computed(() => companyStore.companyInfo);
|
|
||||||
const actualYear = computed(() => new Date().getFullYear());
|
|
||||||
|
|
||||||
const encodedCompanyAddress = computed(() => {
|
|
||||||
return companyInfo.value?.companyAddress ? encodeURIComponent(companyInfo.value?.companyAddress) : '';
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.footer {
|
|
||||||
background-color: #000;
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
padding-block: 64px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__main {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__left {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__logo {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 6.7px;
|
|
||||||
font-family: 'Playfair Display', sans-serif;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
text-shadow: 0 0 5px #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
max-width: 285px;
|
|
||||||
color: #d1d5db;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__columns {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
|
|
||||||
& h6 {
|
|
||||||
color: #d2d0d0;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& p, a {
|
|
||||||
color: $link_secondary;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $link_secondary_hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__bottom {
|
|
||||||
& p {
|
|
||||||
color: $link_secondary;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,264 +0,0 @@
|
||||||
<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 type {ICategory} from '@types';
|
|
||||||
import {onClickOutside} from '@vueuse/core';
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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: $less_border_radius;
|
|
||||||
background-color: rgba($primary, 0.2);
|
|
||||||
border: 1px solid $primary;
|
|
||||||
padding: 5px 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
color: $primary;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $primary;
|
|
||||||
color: $main;
|
|
||||||
}
|
|
||||||
|
|
||||||
& span {
|
|
||||||
font-size: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: $primary;
|
|
||||||
color: $main;
|
|
||||||
|
|
||||||
& span {
|
|
||||||
transform: rotate(-180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 110%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.categories {
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
width: 100%;
|
|
||||||
background-color: $main;
|
|
||||||
box-shadow: 0 0 15px 1px $secondary;
|
|
||||||
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: 50vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__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;
|
|
||||||
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__left {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding-block: 10px;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
& p {
|
|
||||||
cursor: pointer;
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 10px;
|
|
||||||
border-left: 3px solid $main;
|
|
||||||
font-weight: 700;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-color: $primary;
|
|
||||||
color: $primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__main {
|
|
||||||
padding: 20px;
|
|
||||||
border-left: 2px solid $primary_dark;
|
|
||||||
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: $primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__empty {
|
|
||||||
& p {
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,362 +0,0 @@
|
||||||
<template>
|
|
||||||
<header
|
|
||||||
class="header"
|
|
||||||
:class="[{
|
|
||||||
'header__no-search': !uiConfig.showSearchBar,
|
|
||||||
'header__fixed': uiConfig.isHeaderFixed
|
|
||||||
}]"
|
|
||||||
>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header__wrapper">
|
|
||||||
<div class="header__inner">
|
|
||||||
<nuxt-link-locale to="/" class="header__logo">
|
|
||||||
SCHON
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</div>
|
|
||||||
<div class="header__inner">
|
|
||||||
<nav class="header__nav">
|
|
||||||
<nuxt-link-locale
|
|
||||||
to="/shop"
|
|
||||||
class="header__nav-item"
|
|
||||||
:class="[{ active: route.name?.includes('shop') }]"
|
|
||||||
>
|
|
||||||
{{ t('header.nav.shop') }}
|
|
||||||
</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale
|
|
||||||
to="/catalog"
|
|
||||||
class="header__nav-item"
|
|
||||||
:class="[{ active: route.name?.includes('catalog') }]"
|
|
||||||
>
|
|
||||||
{{ t('header.nav.catalog') }}
|
|
||||||
</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale
|
|
||||||
to="/brands"
|
|
||||||
class="header__nav-item"
|
|
||||||
:class="[{ active: route.name?.includes('brands') }]"
|
|
||||||
>
|
|
||||||
{{ t('header.nav.brands') }}
|
|
||||||
</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale
|
|
||||||
to="/blog"
|
|
||||||
class="header__nav-item"
|
|
||||||
:class="[{ active: route.name?.includes('blog') }]"
|
|
||||||
>
|
|
||||||
{{ t('header.nav.blog') }}
|
|
||||||
</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale
|
|
||||||
to="/contact"
|
|
||||||
class="header__nav-item"
|
|
||||||
:class="[{ active: route.name?.includes('contact') }]"
|
|
||||||
>
|
|
||||||
{{ t('header.nav.contact') }}
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div class="header__inner">
|
|
||||||
<div class="header__block">
|
|
||||||
<icon
|
|
||||||
v-if="uiConfig.showSearchBar"
|
|
||||||
@click="isSearchVisible = true"
|
|
||||||
class="header__block-search"
|
|
||||||
name="tabler:search"
|
|
||||||
size="20"
|
|
||||||
/>
|
|
||||||
<ui-language-switcher />
|
|
||||||
<ui-theme-toggle />
|
|
||||||
<el-badge :value="productsInWishlistQuantity">
|
|
||||||
<nuxt-link-locale to="/wishlist">
|
|
||||||
<icon class="header__block-wishlist" name="material-symbols:favorite-rounded" size="20" />
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</el-badge>
|
|
||||||
<el-badge :value="productsInCartQuantity">
|
|
||||||
<nuxt-link-locale to="/cart">
|
|
||||||
<icon class="header__block-cart" name="bx:bxs-shopping-bag" size="20" />
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</el-badge>
|
|
||||||
<nuxt-link-locale
|
|
||||||
to="/profile/settings"
|
|
||||||
class="header__block-item"
|
|
||||||
v-if="isAuthenticated"
|
|
||||||
>
|
|
||||||
<nuxt-img
|
|
||||||
class="header__block-avatar"
|
|
||||||
v-if="user?.avatar"
|
|
||||||
:src="user?.avatar"
|
|
||||||
alt="avatar"
|
|
||||||
format="webp"
|
|
||||||
densities="x1"
|
|
||||||
/>
|
|
||||||
<div class="header__block-profile" v-else>
|
|
||||||
<icon name="clarity:avatar-line" size="16" />
|
|
||||||
</div>
|
|
||||||
</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale
|
|
||||||
to="/auth/sign-in"
|
|
||||||
class="header__block-auth"
|
|
||||||
v-else
|
|
||||||
>
|
|
||||||
<p>{{ t('buttons.login') }}</p>
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="header__search" :class="[{ active: isSearchVisible && uiConfig.showSearchBar }]">
|
|
||||||
<ui-search
|
|
||||||
ref="searchRef"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useProjectConfig } from "@composables/config";
|
|
||||||
import {onClickOutside} from "@vueuse/core";
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const localePath = useLocalePath();
|
|
||||||
const route = useRoute();
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const wishlistStore = useWishlistStore();
|
|
||||||
const cartStore = useCartStore();
|
|
||||||
const { $appHelpers } = useNuxtApp();
|
|
||||||
|
|
||||||
const { uiConfig } = useProjectConfig();
|
|
||||||
|
|
||||||
const isAuthenticated = computed(() => userStore.isAuthenticated);
|
|
||||||
const user = computed(() => userStore.user);
|
|
||||||
|
|
||||||
const cookieWishlist = useCookie($appHelpers.COOKIES_WISHLIST_KEY, {
|
|
||||||
default: () => [],
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
|
|
||||||
default: () => [],
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
const productsInCartQuantity = computed(() => {
|
|
||||||
if (isAuthenticated.value) {
|
|
||||||
let count = 0;
|
|
||||||
cartStore.currentOrder?.orderProducts?.edges.forEach((el) => {
|
|
||||||
count = count + el.node.quantity;
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
} else {
|
|
||||||
return cookieCart.value.reduce((acc, item) => acc + item.quantity, 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const productsInWishlistQuantity = computed(() => {
|
|
||||||
if (isAuthenticated.value) {
|
|
||||||
return wishlistStore.wishlist ? wishlistStore.wishlist.products.edges.length : 0;
|
|
||||||
} else {
|
|
||||||
return cookieWishlist.value.length
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const isSearchVisible = ref<boolean>(false);
|
|
||||||
const searchRef = ref(null);
|
|
||||||
onClickOutside(searchRef, () => isSearchVisible.value = false);
|
|
||||||
|
|
||||||
const redirectTo = (to) => {
|
|
||||||
if (uiConfig.value.isAuthModals) {
|
|
||||||
appStore.setActiveAuthState(to);
|
|
||||||
} else {
|
|
||||||
navigateTo(localePath(`/auth/ + ${to}`));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.header {
|
|
||||||
position: relative;
|
|
||||||
z-index: 5;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
background-color: $main;
|
|
||||||
border-bottom: 1px solid $border;
|
|
||||||
|
|
||||||
&__fixed {
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 25px;
|
|
||||||
padding-block: 25px;
|
|
||||||
background-color: $main;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__inner {
|
|
||||||
width: 33%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__logo {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 6.7px;
|
|
||||||
font-family: 'Playfair Display', sans-serif;
|
|
||||||
color: $primary_dark;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
text-shadow: 0 0 5px $primary_dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__nav {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 40px;
|
|
||||||
|
|
||||||
&-item {
|
|
||||||
position: relative;
|
|
||||||
color: $link_primary;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
bottom: -3px;
|
|
||||||
left: 0;
|
|
||||||
height: 2px;
|
|
||||||
width: 0;
|
|
||||||
transition: all .3s ease;
|
|
||||||
background-color: $link_primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active::after {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
&::after {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__block {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
&-block {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-search {
|
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
|
||||||
color: $secondary;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $secondary_hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-wishlist {
|
|
||||||
display: block;
|
|
||||||
cursor: pointer;
|
|
||||||
color: $secondary;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $secondary_hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-cart {
|
|
||||||
display: block;
|
|
||||||
cursor: pointer;
|
|
||||||
color: $secondary;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $secondary_hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-auth {
|
|
||||||
border-bottom: 1px solid $secondary;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
color: $secondary;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $secondary_hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-avatar {
|
|
||||||
width: 28px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid $secondary;
|
|
||||||
|
|
||||||
& span {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-profile {
|
|
||||||
width: 28px;
|
|
||||||
border-radius: 50%;
|
|
||||||
padding: 5px;
|
|
||||||
border: 1px solid $secondary;
|
|
||||||
|
|
||||||
& span {
|
|
||||||
color: $primary;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__search {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 0fr;
|
|
||||||
//transition: grid-template-rows 0.2s ease;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
<template>
|
|
||||||
<nuxt-link-locale
|
|
||||||
class="card"
|
|
||||||
:to="`/brand/${brand.slug}`"
|
|
||||||
>
|
|
||||||
<nuxt-img
|
|
||||||
v-if="brand.smallLogo"
|
|
||||||
format="webp"
|
|
||||||
densities="x1"
|
|
||||||
:src="brand.smallLogo"
|
|
||||||
:alt="brand.name"
|
|
||||||
class="card__image"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div class="card__image-placeholder" v-else />
|
|
||||||
<p>{{ brand.name }}</p>
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type {IBrand} from '@types';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
brand: IBrand;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.card {
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
background-color: $main;
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
border: 1px solid $border;
|
|
||||||
height: 100%;
|
|
||||||
padding: 23px;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
box-shadow: 0 0 10px 2px $primary_shadow_hover;
|
|
||||||
border-color: $border_hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
object-fit: contain;
|
|
||||||
object-position: center;
|
|
||||||
|
|
||||||
&-placeholder {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
background-color: $primary;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& p {
|
|
||||||
color: $primary_dark;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
<template>
|
|
||||||
<nuxt-link-locale
|
|
||||||
class="card"
|
|
||||||
:to="`/catalog/${category.slug}`"
|
|
||||||
>
|
|
||||||
<nuxt-img
|
|
||||||
v-if="category.image"
|
|
||||||
format="webp"
|
|
||||||
densities="x1"
|
|
||||||
:src="category.image"
|
|
||||||
:alt="category.name"
|
|
||||||
class="card__image"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div class="card__image-placeholder" v-else />
|
|
||||||
<p>{{ category.name }}</p>
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type {ICategory} from '@types';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
category: ICategory;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.card {
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
background-color: $main;
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
border: 1px solid $border;
|
|
||||||
height: 100%;
|
|
||||||
padding: 23px;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
box-shadow: 0 0 10px 2px $primary_shadow_hover;
|
|
||||||
border-color: $border_hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
object-fit: contain;
|
|
||||||
object-position: center;
|
|
||||||
|
|
||||||
&-placeholder {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
background-color: $primary;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& p {
|
|
||||||
color: $primary_dark;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,258 +0,0 @@
|
||||||
<template>
|
|
||||||
<el-collapse-item
|
|
||||||
class="order"
|
|
||||||
>
|
|
||||||
<template #title="{ isActive }">
|
|
||||||
<div :class="['order__top', { 'is-active': isActive }]">
|
|
||||||
<div>
|
|
||||||
<p>{{ t('profile.orders.id') }}: {{ order.humanReadableId }}</p>
|
|
||||||
<p v-if="order.buyTime">{{ useDate(order.buyTime, locale) }}</p>
|
|
||||||
<el-tooltip
|
|
||||||
:content="order.status"
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<p class="status" :style="[{ backgroundColor: statusColor(order.status) }]">
|
|
||||||
{{ order.status }}
|
|
||||||
<icon name="material-symbols:info-outline-rounded" size="14" />
|
|
||||||
</p>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
<icon
|
|
||||||
name="material-symbols:keyboard-arrow-down"
|
|
||||||
size="22"
|
|
||||||
class="order__top-icon"
|
|
||||||
:class="[{ active: isActive }]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="order__top-bottom" :class="{ active: !isActive }">
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<nuxt-img
|
|
||||||
v-for="product in order.orderProducts.edges"
|
|
||||||
:key="product.node.uuid"
|
|
||||||
:src="product.node.product.images.edges[0].node.image"
|
|
||||||
:alt="product.node.product.name"
|
|
||||||
format="webp"
|
|
||||||
densities="x1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p>{{ order.totalPrice }}{{ CURRENCY }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="order__main">
|
|
||||||
<div
|
|
||||||
class="order__product"
|
|
||||||
v-for="product in order.orderProducts.edges"
|
|
||||||
:key="product.node.uuid"
|
|
||||||
>
|
|
||||||
<div class="order__product-left">
|
|
||||||
<nuxt-img
|
|
||||||
:src="product.node.product.images.edges[0].node.image"
|
|
||||||
:alt="product.node.product.name"
|
|
||||||
format="webp"
|
|
||||||
densities="x1"
|
|
||||||
/>
|
|
||||||
<p>{{ product.node.product.name }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="order__product-right">
|
|
||||||
<h6>{{ t('profile.orders.price') }}: {{ product.node.product.price * product.node.quantity }}{{ CURRENCY }}</h6>
|
|
||||||
<p>{{ product.node.quantity }} X {{ product.node.product.price }}$</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="order__total">
|
|
||||||
<p>{{ t('profile.orders.total') }}: {{ order.totalPrice }}$</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-collapse-item>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useDate} from '@composables/date';
|
|
||||||
import {orderStatuses, CURRENCY} from '@appConstants';
|
|
||||||
import type {IOrder} from '@types';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
order: IOrder;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const {t, locale} = useI18n();
|
|
||||||
|
|
||||||
const statusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case orderStatuses.FAILED:
|
|
||||||
return '#FF0000';
|
|
||||||
|
|
||||||
case orderStatuses.PAYMENT:
|
|
||||||
return '#FFC107';
|
|
||||||
|
|
||||||
case orderStatuses.CREATED:
|
|
||||||
return '#007BFF';
|
|
||||||
|
|
||||||
case orderStatuses.DELIVERING:
|
|
||||||
return '#00C853';
|
|
||||||
|
|
||||||
case orderStatuses.FINISHED:
|
|
||||||
return '#00C853';
|
|
||||||
|
|
||||||
case orderStatuses.MOMENTAL:
|
|
||||||
return '#00C853';
|
|
||||||
|
|
||||||
default:
|
|
||||||
return '#000';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.order {
|
|
||||||
&__top {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 0 10px 5px 10px;
|
|
||||||
|
|
||||||
& div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 25px;
|
|
||||||
|
|
||||||
& p {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
|
|
||||||
color: $secondary;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
&.status {
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
padding: 3px 7px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-icon {
|
|
||||||
color: $secondary;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
transform: rotate(-180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-bottom {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 0fr;
|
|
||||||
transition: grid-template-rows 0.2s ease;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
& div {
|
|
||||||
& div {
|
|
||||||
padding-top: 10px;
|
|
||||||
border-top: 1px solid $border;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 25px;
|
|
||||||
|
|
||||||
& div {
|
|
||||||
padding-top: 0;
|
|
||||||
border: none;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
& img {
|
|
||||||
height: 65px;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& p {
|
|
||||||
color: $secondary;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__product {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
border-bottom: 2px solid $border;
|
|
||||||
|
|
||||||
&-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
& img {
|
|
||||||
height: 150px;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
& p {
|
|
||||||
color: $secondary;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-right {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
|
|
||||||
& h6 {
|
|
||||||
color: $secondary;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
& p {
|
|
||||||
color: $secondary;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__total {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
& p {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-collapse-item__header) {
|
|
||||||
height: fit-content;
|
|
||||||
padding-block: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="card">
|
|
||||||
<p class="card__title">{{ post.title }}</p>
|
|
||||||
<nuxt-link-locale :to="`/blog/${post.slug}`" class="card__button">{{ t('buttons.readMore') }}</nuxt-link-locale>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type {IPost} from '@types';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
post: IPost;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const {t} = useI18n();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
word-break: break-all;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
color: $primary_dark;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
width: fit-content;
|
|
||||||
position: relative;
|
|
||||||
color: $primary_dark;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
bottom: -3px;
|
|
||||||
left: 0;
|
|
||||||
height: 2px;
|
|
||||||
width: 0;
|
|
||||||
transition: all .3s ease;
|
|
||||||
background-color: $primary_dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active::after {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
&::after {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,518 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="card"
|
|
||||||
:class="{ 'card__list': isList }"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="card__wishlist"
|
|
||||||
@click="overwriteWishlist({
|
|
||||||
type: (isProductInWishlist ? 'remove' : 'add'),
|
|
||||||
productUuid: product.uuid,
|
|
||||||
productName: product.name,
|
|
||||||
})"
|
|
||||||
>
|
|
||||||
<icon style="color: #dc2626;" name="mdi:cards-heart" size="16" v-if="isProductInWishlist" />
|
|
||||||
<icon name="mdi:cards-heart-outline" size="16" v-else />
|
|
||||||
</div>
|
|
||||||
<div class="card__wrapper">
|
|
||||||
<nuxt-link-locale
|
|
||||||
:to="`/product/${product.slug}`"
|
|
||||||
class="card__link"
|
|
||||||
>
|
|
||||||
<div class="card__block">
|
|
||||||
<client-only>
|
|
||||||
<swiper
|
|
||||||
v-if="images.length"
|
|
||||||
@swiper="onSwiper"
|
|
||||||
:modules="[EffectFade, Pagination]"
|
|
||||||
effect="fade"
|
|
||||||
:slides-per-view="1"
|
|
||||||
:pagination="paginationOptions"
|
|
||||||
class="card__swiper"
|
|
||||||
>
|
|
||||||
<swiper-slide
|
|
||||||
v-for="(img, i) in images"
|
|
||||||
:key="i"
|
|
||||||
class="card__swiper-slide"
|
|
||||||
>
|
|
||||||
<nuxt-img
|
|
||||||
:src="img"
|
|
||||||
:alt="product.name"
|
|
||||||
loading="lazy"
|
|
||||||
class="card__swiper-image"
|
|
||||||
format="webp"
|
|
||||||
densities="x1"
|
|
||||||
/>
|
|
||||||
</swiper-slide>
|
|
||||||
</swiper>
|
|
||||||
<div class="card__image-placeholder" />
|
|
||||||
<div
|
|
||||||
v-for="(image, idx) in images"
|
|
||||||
:key="idx"
|
|
||||||
class="card__block-hover"
|
|
||||||
:style="{ left: `${(100/ images.length) * idx}%`, width: `${100/ images.length}%` }"
|
|
||||||
@mouseenter="goTo(idx)"
|
|
||||||
@mouseleave="goTo(0)"
|
|
||||||
/>
|
|
||||||
</client-only>
|
|
||||||
</div>
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</div>
|
|
||||||
<div class="card__content">
|
|
||||||
<div class="card__content-inner">
|
|
||||||
<div class="card__brand">{{ product.brand.name }}</div>
|
|
||||||
<p class="card__name">{{ product.name }}</p>
|
|
||||||
<el-rate
|
|
||||||
class="card__rating"
|
|
||||||
v-model="rating"
|
|
||||||
size="large"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<div class="card__price">{{ product.price }} $</div>
|
|
||||||
</div>
|
|
||||||
<div class="card__bottom">
|
|
||||||
<div class="card__bottom-inner">
|
|
||||||
<div class="tools" v-if="isProductInCart">
|
|
||||||
<button
|
|
||||||
class="tools__item tools__item-button"
|
|
||||||
@click="overwriteOrder({
|
|
||||||
type: 'remove',
|
|
||||||
productUuid: product.uuid,
|
|
||||||
productName: product.name
|
|
||||||
})"
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</button>
|
|
||||||
<span class="tools__item tools__item-count" v-text="'X' + productInCartQuantity" />
|
|
||||||
<button
|
|
||||||
class="tools__item tools__item-button"
|
|
||||||
@click="overwriteOrder({
|
|
||||||
type: 'add',
|
|
||||||
productUuid: product.uuid,
|
|
||||||
productName: product.name
|
|
||||||
})"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ui-button
|
|
||||||
v-else
|
|
||||||
class="card__bottom-button"
|
|
||||||
@click="overwriteOrder({
|
|
||||||
type: 'add',
|
|
||||||
productUuid: product.uuid,
|
|
||||||
productName: product.name
|
|
||||||
})"
|
|
||||||
:type="'button'"
|
|
||||||
:isLoading="addLoading"
|
|
||||||
>
|
|
||||||
{{ t('buttons.addToCart') }}
|
|
||||||
</ui-button>
|
|
||||||
<ui-button
|
|
||||||
:type="'button'"
|
|
||||||
class="card__bottom-button"
|
|
||||||
:style="'secondary'"
|
|
||||||
@click="buyProduct(product.uuid)"
|
|
||||||
:isLoading="buyLoading"
|
|
||||||
>
|
|
||||||
{{ t('buttons.buyNow') }}
|
|
||||||
</ui-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type {IProduct} from '@types';
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/vue';
|
|
||||||
import { EffectFade, Pagination } from 'swiper/modules';
|
|
||||||
import {useWishlistOverwrite} from '@composables/wishlist';
|
|
||||||
import {useOrderOverwrite} from '@composables/orders';
|
|
||||||
import {useProductBuy} from "@composables/products";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
product: IProduct;
|
|
||||||
isList?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const {t} = useI18n();
|
|
||||||
const wishlistStore = useWishlistStore();
|
|
||||||
const cartStore = useCartStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const { $appHelpers } = useNuxtApp();
|
|
||||||
|
|
||||||
const { overwriteWishlist } = useWishlistOverwrite();
|
|
||||||
const { addLoading, removeLoading, overwriteOrder } = useOrderOverwrite();
|
|
||||||
const { buyProduct, loading: buyLoading } = useProductBuy();
|
|
||||||
|
|
||||||
const cookieWishlist = useCookie($appHelpers.COOKIES_WISHLIST_KEY, {
|
|
||||||
default: () => [],
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
|
|
||||||
default: () => [],
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
const isAuthenticated = computed(() => userStore.isAuthenticated);
|
|
||||||
|
|
||||||
const isProductInWishlist = computed(() => {
|
|
||||||
if (isAuthenticated.value) {
|
|
||||||
return !!wishlistStore.wishlist?.products?.edges.find(
|
|
||||||
(el) => el?.node?.uuid === props.product.uuid
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (cookieWishlist.value ?? []).includes(props.product.uuid);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const isProductInCart = computed(() => {
|
|
||||||
if (isAuthenticated.value) {
|
|
||||||
return !!cartStore.currentOrder?.orderProducts?.edges.find(
|
|
||||||
(prod) => prod.node.product.uuid === props.product?.uuid
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (cookieCart.value ?? []).some(
|
|
||||||
(item) => item.productUuid === props.product?.uuid
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const productInCartQuantity = computed(() => {
|
|
||||||
if (isAuthenticated.value) {
|
|
||||||
const productEdge = cartStore.currentOrder?.orderProducts?.edges.find(
|
|
||||||
(prod) => prod.node.product.uuid === props.product.uuid
|
|
||||||
);
|
|
||||||
return productEdge?.node.quantity ?? 0;
|
|
||||||
} else {
|
|
||||||
const cartItem = (cookieCart.value ?? []).find(
|
|
||||||
(item) => item.productUuid === props.product.uuid
|
|
||||||
);
|
|
||||||
return cartItem?.quantity ?? 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const rating = computed(() => {
|
|
||||||
return props.product.feedbacks.edges[0]?.node?.rating ?? 5;
|
|
||||||
});
|
|
||||||
|
|
||||||
const images = computed(() =>
|
|
||||||
props.product.images.edges.map(e => e.node.image)
|
|
||||||
);
|
|
||||||
const paginationOptions = computed(() =>
|
|
||||||
images.value.length > 1
|
|
||||||
? {
|
|
||||||
clickable: true,
|
|
||||||
bulletClass: 'swiper-pagination-line',
|
|
||||||
bulletActiveClass: 'swiper-pagination-line--active'
|
|
||||||
}
|
|
||||||
: false
|
|
||||||
);
|
|
||||||
|
|
||||||
const swiperRef = ref<any>(null);
|
|
||||||
|
|
||||||
function onSwiper(swiper: any) {
|
|
||||||
swiperRef.value = swiper;
|
|
||||||
}
|
|
||||||
|
|
||||||
function goTo(index: number) {
|
|
||||||
swiperRef.value?.slideTo(index);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.card {
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
border: 1px solid $border;
|
|
||||||
width: 100%;
|
|
||||||
background-color: $main;
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&__list {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 50px;
|
|
||||||
padding: 15px;
|
|
||||||
|
|
||||||
& .card__link {
|
|
||||||
width: fit-content;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .card__block {
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .card__content {
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&-inner {
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .tools {
|
|
||||||
width: 136px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .card__bottom {
|
|
||||||
margin-top: 0;
|
|
||||||
width: fit-content;
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-end;
|
|
||||||
|
|
||||||
&-inner {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-button {
|
|
||||||
width: fit-content;
|
|
||||||
padding-inline: 25px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .card__wrapper {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
border-color: $border_hover;
|
|
||||||
box-shadow: 0 0 10px 1px $border;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__link {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__block {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&-hover {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 20%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 2;
|
|
||||||
cursor: pointer;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__swiper {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
|
|
||||||
&-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
&-placeholder {
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
background-color: $primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
padding: 10px 20px 20px 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
|
|
||||||
&-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__brand {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.2px;
|
|
||||||
color: $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__price {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 16px;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
color: $primary_dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__rating {
|
|
||||||
margin-block: 4px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__name {
|
|
||||||
word-break: break-all;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
color: $primary_dark;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__quantity {
|
|
||||||
width: fit-content;
|
|
||||||
background-color: rgba($secondary, 0.5);
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
padding: 5px 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__wishlist {
|
|
||||||
cursor: pointer;
|
|
||||||
width: fit-content;
|
|
||||||
background-color: $main;
|
|
||||||
box-shadow: 0 2px 4px 0 $primary_shadow;
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
right: 16px;
|
|
||||||
z-index: 3;
|
|
||||||
border-radius: 50%;
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
box-shadow: 0 2px 4px 0 $primary_shadow_hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
& span {
|
|
||||||
color: $secondary;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__bottom {
|
|
||||||
&-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-button {
|
|
||||||
padding-block: 10px !important;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-wishlist {
|
|
||||||
cursor: pointer;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background-color: $primary;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
|
|
||||||
font-size: 22px;
|
|
||||||
color: $main;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $primary_hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tools {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
background-color: $primary;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 2fr 1fr;
|
|
||||||
height: 40px;
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
&-count {
|
|
||||||
border-left: 1px solid $primary_hover;
|
|
||||||
border-right: 1px solid $primary_hover;
|
|
||||||
|
|
||||||
color: $main;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-button {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: $primary;
|
|
||||||
border-radius: $less_border_radius 0 0 $less_border_radius;
|
|
||||||
|
|
||||||
color: $main;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $primary_hover;
|
|
||||||
color: $main;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-radius: 0 $less_border_radius $less_border_radius 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.swiper-pagination) {
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.swiper-pagination-line) {
|
|
||||||
display: inline-block;
|
|
||||||
width: 24px;
|
|
||||||
height: 2px;
|
|
||||||
background-color: rgba($primary_dark, 0.3);
|
|
||||||
border-radius: 0;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.swiper-pagination-line--active) {
|
|
||||||
background-color: $primary_dark;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,317 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="isDemoMode && isOpen" class="modal">
|
|
||||||
<div
|
|
||||||
class="modal__wrapper"
|
|
||||||
>
|
|
||||||
<demo-ui-button
|
|
||||||
@click="appStore.setDemoSettings(false)"
|
|
||||||
class="modal__close"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:close" size="30" />
|
|
||||||
</demo-ui-button>
|
|
||||||
<h2 class="modal__title">{{ toConstantCase(t('demo.settings.title')) }}</h2>
|
|
||||||
<div class="modal__inner">
|
|
||||||
<div class="modal__block">
|
|
||||||
<h3 class="modal__block-title">{{ toConstantCase(t('demo.settings.ui')) }}</h3>
|
|
||||||
<div
|
|
||||||
v-for="(flag, idx) in availableFlags"
|
|
||||||
:key="flag.key"
|
|
||||||
class="modal__block-item"
|
|
||||||
>
|
|
||||||
<demo-ui-checkbox
|
|
||||||
:label="toConstantCase(flag.label)"
|
|
||||||
v-model="localFlags[flag.key]"
|
|
||||||
:id="idx"
|
|
||||||
/>
|
|
||||||
<p>{{ flag.description }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<demo-ui-button @click="showCodePreview = !showCodePreview">{{ toConstantCase(t('demo.buttons.generateCode')) }}</demo-ui-button>
|
|
||||||
<div v-if="showCodePreview" class="modal__preview">
|
|
||||||
<div class="modal__preview-wrapper">
|
|
||||||
<pre class="modal__preview-code"><code class="language-typescript">{{ codePreview }}</code></pre>
|
|
||||||
<demo-ui-button
|
|
||||||
@click="copyConfig"
|
|
||||||
class="modal__preview-button"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:content-copy" size="16" />
|
|
||||||
</demo-ui-button>
|
|
||||||
</div>
|
|
||||||
<p class="modal__preview-text">{{ t('demo.preview.text') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal__buttons">
|
|
||||||
<demo-ui-button @click="resetToDefault">
|
|
||||||
<Icon name="material-symbols:refresh" size="20" />
|
|
||||||
{{ toConstantCase(t('demo.buttons.reset')) }}
|
|
||||||
</demo-ui-button>
|
|
||||||
<demo-ui-button @click="saveChanges">
|
|
||||||
<Icon name="material-symbols:save" size="20" />
|
|
||||||
{{ toConstantCase(t('demo.buttons.save')) }}
|
|
||||||
</demo-ui-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useProjectConfig } from '@composables/config';
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const {t} = useI18n();
|
|
||||||
|
|
||||||
const isOpen = computed(() => appStore.isDemoSettings);
|
|
||||||
|
|
||||||
const {
|
|
||||||
isDemoMode,
|
|
||||||
availableFlags,
|
|
||||||
setDemoFlag,
|
|
||||||
resetDemoFlags,
|
|
||||||
uiConfig,
|
|
||||||
generateConfigCode,
|
|
||||||
copyToClipboard
|
|
||||||
} = useProjectConfig();
|
|
||||||
|
|
||||||
const showCodePreview = ref<boolean>(false);
|
|
||||||
const localFlags = ref<Record<string, boolean>>({});
|
|
||||||
|
|
||||||
const previewConfig = computed(() => {
|
|
||||||
const config: Record<string, boolean> = {};
|
|
||||||
availableFlags.value.forEach(flag => {
|
|
||||||
config[flag.key] = localFlags.value[flag.key] !== undefined
|
|
||||||
? localFlags.value[flag.key]
|
|
||||||
: flag.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatPreviewConfig = (config: Record<string, boolean>): string => {
|
|
||||||
const entries = Object.entries(config)
|
|
||||||
.map(([key, value]) => ` ${key}: ${value}`)
|
|
||||||
.join(',\n');
|
|
||||||
|
|
||||||
return `ui: {\n${entries}\n}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
function toConstantCase(text: string): string {
|
|
||||||
const placeholders: string[] = [];
|
|
||||||
let placeholderIndex = 0;
|
|
||||||
|
|
||||||
const textWithPlaceholders = text.replace(/(['"])(.*?)\1/g, (match) => {
|
|
||||||
placeholders.push(match);
|
|
||||||
return `__QUOTE_PLACEHOLDER_${placeholderIndex++}__`;
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = textWithPlaceholders
|
|
||||||
.toUpperCase()
|
|
||||||
.replace(/\s+/g, '_')
|
|
||||||
.replace(/[^A-Z0-9_]/g, '');
|
|
||||||
|
|
||||||
placeholders.forEach((placeholder, index) => {
|
|
||||||
result = result.replace(`__QUOTE_PLACEHOLDER_${index}__`, placeholder);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(isOpen, (newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
const flags: Record<string, boolean> = {};
|
|
||||||
availableFlags.value.forEach(flag => {
|
|
||||||
flags[flag.key] = flag.value;
|
|
||||||
});
|
|
||||||
localFlags.value = flags;
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
const saveChanges = () => {
|
|
||||||
availableFlags.value.forEach(flag => {
|
|
||||||
const newValue = localFlags.value[flag.key];
|
|
||||||
const oldValue = flag.value;
|
|
||||||
|
|
||||||
if (newValue !== oldValue) {
|
|
||||||
setDemoFlag(flag.key, newValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
appStore.setDemoSettings(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetToDefault = () => {
|
|
||||||
resetDemoFlags();
|
|
||||||
appStore.setDemoSettings(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyConfig = async () => {
|
|
||||||
const code = codePreview.value;
|
|
||||||
await copyToClipboard(code);
|
|
||||||
};
|
|
||||||
|
|
||||||
const codePreview = computed(() => {
|
|
||||||
return formatPreviewConfig(previewConfig.value);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.modal {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 3;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 2;
|
|
||||||
inset: 100px;
|
|
||||||
background-color: $primary_dark;
|
|
||||||
padding: 50px;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__close {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
top: 15px;
|
|
||||||
right: 15px;
|
|
||||||
padding: 7px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 40px;
|
|
||||||
font-weight: 500;
|
|
||||||
text-shadow: 0 0 5px $primary_shadow;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__inner {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 50px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
scrollbar-color: $text transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__block {
|
|
||||||
align-self: flex-start;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
|
|
||||||
&-title {
|
|
||||||
width: fit-content;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
border-bottom: 2px solid $text;
|
|
||||||
color: $text;
|
|
||||||
font-weight: 500;
|
|
||||||
text-shadow: 0 0 5px $text;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
|
|
||||||
& p {
|
|
||||||
padding-left: 50px;
|
|
||||||
color: $text;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__preview {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
&-wrapper {
|
|
||||||
position: relative;
|
|
||||||
background-color: $primary_dark;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
padding: 20px;
|
|
||||||
border: 1px solid rgba($text, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-code {
|
|
||||||
margin: 0;
|
|
||||||
color: $main_hover;
|
|
||||||
font-family: 'Fira Code', 'Courier New', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
main-space: pre-wrap;
|
|
||||||
overflow-x: auto;
|
|
||||||
|
|
||||||
.language-typescript {
|
|
||||||
color: #f8f8f2;
|
|
||||||
|
|
||||||
.keyword {
|
|
||||||
color: lch(69.03% 60.03 345.46);
|
|
||||||
}
|
|
||||||
|
|
||||||
.string {
|
|
||||||
color: #f1fa8c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.number {
|
|
||||||
color: #bd93f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boolean {
|
|
||||||
color: #ff79c6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property {
|
|
||||||
color: #8be9fd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
padding: 6px 12px !important;
|
|
||||||
font-size: 12px !important;
|
|
||||||
gap: 6px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-text {
|
|
||||||
color: $text;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__buttons {
|
|
||||||
border-top: 1px solid $main;
|
|
||||||
padding-top: 25px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 25px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="button"
|
|
||||||
:disabled="isDisabled"
|
|
||||||
:class="[{active: isLoading}]"
|
|
||||||
>
|
|
||||||
<ui-loader class="button__loader" v-if="isLoading" />
|
|
||||||
<slot v-else />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{
|
|
||||||
isDisabled?: boolean;
|
|
||||||
isLoading?: boolean;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.button {
|
|
||||||
position: relative;
|
|
||||||
width: fit-content;
|
|
||||||
cursor: pointer;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border: 1px solid $text;
|
|
||||||
box-shadow: 0 0 10px 1px $text;
|
|
||||||
background-color: transparent;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
padding: 7px 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
color: $text;
|
|
||||||
text-shadow: 0 0 10px $text;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $text;
|
|
||||||
color: $main;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: $text;
|
|
||||||
color: $main;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
background-color: transparent;
|
|
||||||
color: $main;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled:hover, &.active {
|
|
||||||
background-color: transparent;
|
|
||||||
color: $main;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__loader {
|
|
||||||
margin-block: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
<template>
|
|
||||||
<label class="checkbox">
|
|
||||||
<input
|
|
||||||
:id="id"
|
|
||||||
type="checkbox"
|
|
||||||
:checked="modelValue"
|
|
||||||
@change="$emit('update:modelValue', ($event.target as HTMLInputElement).checked)"
|
|
||||||
/>
|
|
||||||
<span class="checkbox__box">
|
|
||||||
<span class="checkbox__mark"></span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="checkbox__label"
|
|
||||||
:data-text="label"
|
|
||||||
>
|
|
||||||
{{ label }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{
|
|
||||||
label: string;
|
|
||||||
modelValue: boolean;
|
|
||||||
id: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
'update:modelValue': [value: boolean]
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.checkbox {
|
|
||||||
width: fit-content;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
--glitch-anim-duration: 0.3s;
|
|
||||||
|
|
||||||
& input {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__box {
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1.5em;
|
|
||||||
border: 2px solid $text;
|
|
||||||
position: relative;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
clip-path: polygon(
|
|
||||||
15% 0,
|
|
||||||
85% 0,
|
|
||||||
100% 15%,
|
|
||||||
100% 85%,
|
|
||||||
85% 100%,
|
|
||||||
15% 100%,
|
|
||||||
0 85%,
|
|
||||||
0 15%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__mark {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 60%;
|
|
||||||
height: 60%;
|
|
||||||
background-color: $text;
|
|
||||||
transform: translate(-50%, -50%) scale(0);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
|
|
||||||
clip-path: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
color: $main;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 18px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
position: relative;
|
|
||||||
transition:
|
|
||||||
color 0.3s ease,
|
|
||||||
text-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox input:checked + .checkbox__box .checkbox__mark {
|
|
||||||
transform: translate(-50%, -50%) scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
animation: glitch-anim-checkbox var(--glitch-anim-duration) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox input:checked ~ .checkbox__label {
|
|
||||||
color: $text;
|
|
||||||
text-shadow: 0 0 8px $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox:hover .checkbox__box {
|
|
||||||
box-shadow: 0 0 10px $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox:hover .checkbox__label::before,
|
|
||||||
.checkbox:hover .checkbox__label::after {
|
|
||||||
content: attr(data-text);
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: $primary_dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox:hover .checkbox__label::before {
|
|
||||||
color: $text;
|
|
||||||
animation: glitch-anim-text var(--glitch-anim-duration)
|
|
||||||
cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox:hover .checkbox__label::after {
|
|
||||||
color: $text;
|
|
||||||
animation: glitch-anim-text var(--glitch-anim-duration)
|
|
||||||
cubic-bezier(0.25, 0.46, 0.45, 0.94) reverse both;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch-anim-checkbox {
|
|
||||||
0% {
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
clip-path: inset(0 0 0 0);
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
transform: translate(calc(-50% - 3px), calc(-50% + 2px));
|
|
||||||
clip-path: inset(50% 0 20% 0);
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
transform: translate(calc(-50% + 2px), calc(-50% - 1px));
|
|
||||||
clip-path: inset(20% 0 60% 0);
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
transform: translate(calc(-50% - 2px), calc(-50% + 1px));
|
|
||||||
clip-path: inset(80% 0 5% 0);
|
|
||||||
}
|
|
||||||
80% {
|
|
||||||
transform: translate(calc(-50% + 2px), calc(-50% - 2px));
|
|
||||||
clip-path: inset(30% 0 45% 0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
clip-path: inset(0 0 0 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch-anim-text {
|
|
||||||
0% {
|
|
||||||
transform: translate(0);
|
|
||||||
clip-path: inset(0 0 0 0);
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
transform: translate(-3px, 2px);
|
|
||||||
clip-path: inset(50% 0 20% 0);
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
transform: translate(2px, -1px);
|
|
||||||
clip-path: inset(20% 0 60% 0);
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
transform: translate(-2px, 1px);
|
|
||||||
clip-path: inset(80% 0 5% 0);
|
|
||||||
}
|
|
||||||
80% {
|
|
||||||
transform: translate(2px, -2px);
|
|
||||||
clip-path: inset(30% 0 45% 0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0);
|
|
||||||
clip-path: inset(0 0 0 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
<template>
|
|
||||||
<form @submit.prevent="handleContactUs()" class="form">
|
|
||||||
<h2 class="form__title">{{ t('contact.form.title') }}</h2>
|
|
||||||
<ui-input
|
|
||||||
:type="'text'"
|
|
||||||
:placeholder="t('fields.name')"
|
|
||||||
:label="t('fields.name')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="name"
|
|
||||||
/>
|
|
||||||
<ui-input
|
|
||||||
:type="'email'"
|
|
||||||
:placeholder="t('fields.email')"
|
|
||||||
:label="t('fields.email')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="email"
|
|
||||||
:inputMode="'email'"
|
|
||||||
/>
|
|
||||||
<ui-input
|
|
||||||
:type="'text'"
|
|
||||||
:placeholder="t('fields.phoneNumber')"
|
|
||||||
:label="t('fields.phoneNumber')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="phoneNumber"
|
|
||||||
:inputMode="'tel'"
|
|
||||||
/>
|
|
||||||
<ui-input
|
|
||||||
:type="'text'"
|
|
||||||
:placeholder="t('fields.subject')"
|
|
||||||
:label="t('fields.subject')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="subject"
|
|
||||||
/>
|
|
||||||
<ui-textarea
|
|
||||||
:placeholder="t('fields.message')"
|
|
||||||
:label="t('fields.message')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="message"
|
|
||||||
/>
|
|
||||||
<ui-button
|
|
||||||
:type="'submit'"
|
|
||||||
class="form__button"
|
|
||||||
:isDisabled="!isFormValid"
|
|
||||||
:isLoading="loading"
|
|
||||||
>
|
|
||||||
{{ t('buttons.sendMessage') }}
|
|
||||||
</ui-button>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useValidators} from '@composables/rules';
|
|
||||||
import {useContactUs} from '@composables/contact';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const { required } = useValidators();
|
|
||||||
|
|
||||||
const name = ref<string>('');
|
|
||||||
const email = ref<string>('');
|
|
||||||
const phoneNumber = ref<string>('');
|
|
||||||
const subject = ref<string>('');
|
|
||||||
const message = ref<string>('');
|
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
|
||||||
return (
|
|
||||||
required(name.value) === true &&
|
|
||||||
required(email.value) === true &&
|
|
||||||
required(phoneNumber.value) === true &&
|
|
||||||
required(subject.value) === true &&
|
|
||||||
required(message.value) === true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { contactUs, loading } = useContactUs();
|
|
||||||
|
|
||||||
async function handleContactUs() {
|
|
||||||
await contactUs(
|
|
||||||
name.value,
|
|
||||||
email.value,
|
|
||||||
phoneNumber.value,
|
|
||||||
subject.value,
|
|
||||||
message.value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.form {
|
|
||||||
width: 585px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid $border;
|
|
||||||
padding: 32px;
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
color: $primary_dark;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
<template>
|
|
||||||
<form @submit.prevent="handleDeposit()" class="form">
|
|
||||||
<div class="form__box">
|
|
||||||
<ui-input
|
|
||||||
:type="'text'"
|
|
||||||
:placeholder="''"
|
|
||||||
v-model="amount"
|
|
||||||
:numberOnly="true"
|
|
||||||
:inputMode="'decimal'"
|
|
||||||
/>
|
|
||||||
<icon name="ic:baseline-compare-arrows" size="30" />
|
|
||||||
<ui-input
|
|
||||||
:type="'text'"
|
|
||||||
:placeholder="''"
|
|
||||||
v-model="amount"
|
|
||||||
:numberOnly="true"
|
|
||||||
:inputMode="'decimal'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ui-button
|
|
||||||
:type="'submit'"
|
|
||||||
class="form__button"
|
|
||||||
:isDisabled="!isFormValid"
|
|
||||||
:isLoading="loading"
|
|
||||||
>
|
|
||||||
{{ t('buttons.topUp') }}
|
|
||||||
</ui-button>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useDeposit} from '@composables/user';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const companyStore = useCompanyStore();
|
|
||||||
|
|
||||||
const { paymentMin, paymentMax } = usePaymentLimits();
|
|
||||||
|
|
||||||
const amount = ref<number>(5);
|
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
|
||||||
return (
|
|
||||||
amount.value >= paymentMin.value &&
|
|
||||||
amount.value <= paymentMax.value
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { deposit, loading } = useDeposit();
|
|
||||||
|
|
||||||
async function handleDeposit() {
|
|
||||||
await deposit(amount.value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
&__box {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
& span {
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
width: fit-content;
|
|
||||||
padding-inline: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
<template>
|
|
||||||
<form @submit.prevent="handleLogin()" class="form">
|
|
||||||
<div class="form__top">
|
|
||||||
<h2 class="form__title">{{ t('forms.login.title') }}</h2>
|
|
||||||
<p class="form__subtitle">{{ t('forms.login.subtitle') }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="form__main">
|
|
||||||
<ui-input
|
|
||||||
:type="'email'"
|
|
||||||
:placeholder="t('fields.email')"
|
|
||||||
:label="t('fields.email')"
|
|
||||||
:rules="[isEmail]"
|
|
||||||
v-model="email"
|
|
||||||
:inputMode="'email'"
|
|
||||||
/>
|
|
||||||
<ui-input
|
|
||||||
:type="'password'"
|
|
||||||
:placeholder="t('fields.password')"
|
|
||||||
:label="t('fields.password')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="password"
|
|
||||||
/>
|
|
||||||
<div class="form__main-block">
|
|
||||||
<ui-checkbox
|
|
||||||
v-model="isStayLogin"
|
|
||||||
>
|
|
||||||
{{ t('checkboxes.remember') }}
|
|
||||||
</ui-checkbox>
|
|
||||||
<ui-link
|
|
||||||
@click="redirectTo('reset-password')"
|
|
||||||
>
|
|
||||||
{{ t('forms.login.forgot') }}
|
|
||||||
</ui-link>
|
|
||||||
</div>
|
|
||||||
<ui-button
|
|
||||||
:type="'submit'"
|
|
||||||
class="form__button"
|
|
||||||
:isDisabled="!isFormValid"
|
|
||||||
:isLoading="loading"
|
|
||||||
>
|
|
||||||
{{ t('buttons.login') }}
|
|
||||||
</ui-button>
|
|
||||||
<p class="form__or">{{ t('forms.login.or') }}</p>
|
|
||||||
<ui-button
|
|
||||||
@click="redirectTo('sign-up')"
|
|
||||||
:type="'button'"
|
|
||||||
:style="'secondary'"
|
|
||||||
class="form__button"
|
|
||||||
>
|
|
||||||
{{ t('buttons.createAccount') }}
|
|
||||||
</ui-button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useLogin} from '@composables/auth';
|
|
||||||
import {useValidators} from '@composables/rules';
|
|
||||||
import {useProjectConfig} from "@composables/config";
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const route = useRoute();
|
|
||||||
const localePath = useLocalePath();
|
|
||||||
|
|
||||||
const { uiConfig } = useProjectConfig();
|
|
||||||
const { required, isEmail } = useValidators();
|
|
||||||
|
|
||||||
const email = ref<string>('');
|
|
||||||
const password = ref<string>('');
|
|
||||||
const isStayLogin = ref<boolean>(false);
|
|
||||||
|
|
||||||
const redirectTo = (to) => {
|
|
||||||
if (uiConfig.value.isAuthModals) {
|
|
||||||
appStore.setActiveAuthState(to);
|
|
||||||
} else {
|
|
||||||
navigateTo(localePath(`/auth/${to}`));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
|
||||||
return (
|
|
||||||
isEmail(email.value) === true &&
|
|
||||||
required(password.value) === true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { login, loading } = useLogin();
|
|
||||||
|
|
||||||
async function handleLogin() {
|
|
||||||
await login(email.value, password.value, isStayLogin.value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.form {
|
|
||||||
width: 450px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 30px;
|
|
||||||
|
|
||||||
&__top {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
color: $primary;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subtitle {
|
|
||||||
text-align: center;
|
|
||||||
color: $text;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__main {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
|
|
||||||
&-block {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__or {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
color: $text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
<template>
|
|
||||||
<form @submit.prevent="handleReset()" class="form">
|
|
||||||
<div class="form__top">
|
|
||||||
<h2 class="form__title">{{ t('forms.newPassword.title') }}</h2>
|
|
||||||
</div>
|
|
||||||
<ui-input
|
|
||||||
:type="'password'"
|
|
||||||
:placeholder="t('fields.newPassword')"
|
|
||||||
:label="t('fields.newPassword')"
|
|
||||||
:rules="[isPasswordValid]"
|
|
||||||
v-model="password"
|
|
||||||
/>
|
|
||||||
<ui-input
|
|
||||||
:type="'password'"
|
|
||||||
:placeholder="t('fields.confirmNewPassword')"
|
|
||||||
:label="t('fields.confirmNewPassword')"
|
|
||||||
:rules="[compareStrings]"
|
|
||||||
v-model="confirmPassword"
|
|
||||||
/>
|
|
||||||
<ui-button
|
|
||||||
:type="'submit'"
|
|
||||||
class="form__button"
|
|
||||||
:isDisabled="!isFormValid"
|
|
||||||
:isLoading="loading"
|
|
||||||
>
|
|
||||||
{{ t('buttons.save') }}
|
|
||||||
</ui-button>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useValidators} from '@composables/rules';
|
|
||||||
import {useNewPassword} from '@composables/auth';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const { isPasswordValid } = useValidators();
|
|
||||||
|
|
||||||
const password = ref<string>('');
|
|
||||||
const confirmPassword = ref<string>('');
|
|
||||||
|
|
||||||
const compareStrings = (v: string) => {
|
|
||||||
if (v === password.value) return true;
|
|
||||||
return t('errors.compare');
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
|
||||||
return (
|
|
||||||
isPasswordValid(password.value) === true &&
|
|
||||||
compareStrings(confirmPassword.value) === true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { newPassword, loading } = useNewPassword();
|
|
||||||
|
|
||||||
async function handleReset() {
|
|
||||||
await newPassword(
|
|
||||||
password.value,
|
|
||||||
confirmPassword.value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.form {
|
|
||||||
width: 450px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 30px;
|
|
||||||
|
|
||||||
&__top {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
color: $primary;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subtitle {
|
|
||||||
text-align: center;
|
|
||||||
color: $text;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__main {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
|
|
||||||
&-block {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
<template>
|
|
||||||
<form @submit.prevent="handleRegister()" class="form">
|
|
||||||
<div class="form__top">
|
|
||||||
<h2 class="form__title">{{ t('forms.register.title') }}</h2>
|
|
||||||
<p class="form__subtitle">{{ t('forms.register.subtitle') }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="form__main">
|
|
||||||
<div class="form__main-box">
|
|
||||||
<ui-input
|
|
||||||
:type="'text'"
|
|
||||||
:placeholder="t('fields.firstName')"
|
|
||||||
:label="t('fields.firstName')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="firstName"
|
|
||||||
/>
|
|
||||||
<ui-input
|
|
||||||
:type="'text'"
|
|
||||||
:placeholder="t('fields.lastName')"
|
|
||||||
:label="t('fields.lastName')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="lastName"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form__main-box">
|
|
||||||
<ui-input
|
|
||||||
:type="'text'"
|
|
||||||
:placeholder="t('fields.phoneNumber')"
|
|
||||||
:label="t('fields.phoneNumber')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="phoneNumber"
|
|
||||||
:inputMode="'tel'"
|
|
||||||
/>
|
|
||||||
<ui-input
|
|
||||||
:type="'email'"
|
|
||||||
:placeholder="t('fields.email')"
|
|
||||||
:label="t('fields.email')"
|
|
||||||
:rules="[isEmail]"
|
|
||||||
v-model="email"
|
|
||||||
:inputMode="'email'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ui-input
|
|
||||||
:type="'password'"
|
|
||||||
:placeholder="t('fields.password')"
|
|
||||||
:label="t('fields.password')"
|
|
||||||
:rules="[isPasswordValid]"
|
|
||||||
v-model="password"
|
|
||||||
/>
|
|
||||||
<ui-input
|
|
||||||
:type="'password'"
|
|
||||||
:placeholder="t('fields.confirmPassword')"
|
|
||||||
:label="t('fields.confirmPassword')"
|
|
||||||
:rules="[compareStrings]"
|
|
||||||
v-model="confirmPassword"
|
|
||||||
/>
|
|
||||||
<ui-checkbox
|
|
||||||
v-model="isSubscribed"
|
|
||||||
>
|
|
||||||
{{ t('checkboxes.subscribe') }}
|
|
||||||
</ui-checkbox>
|
|
||||||
<ui-checkbox
|
|
||||||
v-model="isAgree"
|
|
||||||
>
|
|
||||||
<i18n-t tag="p" scope="global" keypath="checkboxes.agree">
|
|
||||||
<template #terms>
|
|
||||||
<nuxt-link-locale
|
|
||||||
class="form__link"
|
|
||||||
to="/docs/terms-of-use"
|
|
||||||
>
|
|
||||||
{{ t('docs.terms.title') }}
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</template>
|
|
||||||
<template #policy>
|
|
||||||
<nuxt-link-locale
|
|
||||||
class="form__link"
|
|
||||||
to="/docs/privacy-policy"
|
|
||||||
>
|
|
||||||
{{ t('docs.policy.title') }}
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</ui-checkbox>
|
|
||||||
<ui-button
|
|
||||||
:type="'submit'"
|
|
||||||
class="form__button"
|
|
||||||
:isDisabled="!isFormValid"
|
|
||||||
:isLoading="loading"
|
|
||||||
>
|
|
||||||
{{ t('buttons.createAccount') }}
|
|
||||||
</ui-button>
|
|
||||||
<div class="form__login">
|
|
||||||
<p>{{ t('forms.register.login') }}</p>
|
|
||||||
<ui-link
|
|
||||||
@click="redirectTo('sign-in')"
|
|
||||||
>
|
|
||||||
{{ t('buttons.login') }}
|
|
||||||
</ui-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useValidators} from '@composables/rules';
|
|
||||||
import {useRegister} from '@composables/auth';
|
|
||||||
import {useRouteQuery} from '@vueuse/router';
|
|
||||||
import {useProjectConfig} from "@composables/config";
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const route = useRoute();
|
|
||||||
const localePath = useLocalePath();
|
|
||||||
|
|
||||||
const { uiConfig } = useProjectConfig();
|
|
||||||
const { required, isEmail, isPasswordValid } = useValidators();
|
|
||||||
|
|
||||||
const isAgree = ref<boolean>(false);
|
|
||||||
const isSubscribed = ref<boolean>(false);
|
|
||||||
const firstName = ref<string>('');
|
|
||||||
const lastName = ref<string>('');
|
|
||||||
const phoneNumber = ref<string>('');
|
|
||||||
const email = ref<string>('');
|
|
||||||
const password = ref<string>('');
|
|
||||||
const confirmPassword = ref<string>('');
|
|
||||||
|
|
||||||
const referrer = useRouteQuery('referrer', '');
|
|
||||||
|
|
||||||
const redirectTo = (to) => {
|
|
||||||
if (uiConfig.value.isAuthModals) {
|
|
||||||
appStore.setActiveAuthState(to);
|
|
||||||
} else {
|
|
||||||
navigateTo(localePath(`/auth/${to}`));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const compareStrings = (v: string) => {
|
|
||||||
if (v === password.value) return true;
|
|
||||||
return t('errors.compare');
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
|
||||||
return (
|
|
||||||
required(firstName.value) === true &&
|
|
||||||
required(lastName.value) === true &&
|
|
||||||
required(phoneNumber.value) === true &&
|
|
||||||
isEmail(email.value) === true &&
|
|
||||||
isPasswordValid(password.value) === true &&
|
|
||||||
compareStrings(confirmPassword.value) === true &&
|
|
||||||
isAgree.value === true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { register, loading } = useRegister();
|
|
||||||
|
|
||||||
async function handleRegister() {
|
|
||||||
await register({
|
|
||||||
firstName: firstName.value,
|
|
||||||
lastName: lastName.value,
|
|
||||||
phoneNumber: phoneNumber.value,
|
|
||||||
email: email.value,
|
|
||||||
password: password.value,
|
|
||||||
confirmPassword: confirmPassword.value,
|
|
||||||
referrer: referrer.value,
|
|
||||||
isSubscribed: isSubscribed.value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.form {
|
|
||||||
width: 450px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 30px;
|
|
||||||
|
|
||||||
&__top {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
color: $primary;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subtitle {
|
|
||||||
text-align: center;
|
|
||||||
color: $text;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__main {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
|
|
||||||
&-block {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-box {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__link {
|
|
||||||
color: $primary;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $primary_hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__login {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
|
|
||||||
& p {
|
|
||||||
text-align: center;
|
|
||||||
color: $text;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
<template>
|
|
||||||
<form @submit.prevent="handleReset()" class="form">
|
|
||||||
<div class="form__top">
|
|
||||||
<h2 class="form__title">{{ t('forms.reset.title') }}</h2>
|
|
||||||
<p class="form__subtitle">{{ t('forms.reset.subtitle') }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="form__main">
|
|
||||||
<ui-input
|
|
||||||
:type="'email'"
|
|
||||||
:placeholder="t('fields.email')"
|
|
||||||
:label="t('fields.email')"
|
|
||||||
:rules="[isEmail]"
|
|
||||||
v-model="email"
|
|
||||||
:inputMode="'email'"
|
|
||||||
/>
|
|
||||||
<ui-button
|
|
||||||
:type="'submit'"
|
|
||||||
class="form__button"
|
|
||||||
:isDisabled="!isFormValid"
|
|
||||||
:isLoading="loading"
|
|
||||||
>
|
|
||||||
{{ t('buttons.sendLink') }}
|
|
||||||
</ui-button>
|
|
||||||
<ui-link
|
|
||||||
@click="redirectTo('sign-in')"
|
|
||||||
>
|
|
||||||
<icon name="material-symbols:arrow-left-alt" size="20" />
|
|
||||||
{{ t('forms.reset.backToLogin') }}
|
|
||||||
</ui-link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useValidators} from '@composables/rules';
|
|
||||||
import {usePasswordReset} from '@composables/auth';
|
|
||||||
import {useProjectConfig} from "@composables/config";
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const localePath = useLocalePath();
|
|
||||||
|
|
||||||
const { uiConfig } = useProjectConfig();
|
|
||||||
const { isEmail } = useValidators();
|
|
||||||
|
|
||||||
const email = ref<string>('');
|
|
||||||
|
|
||||||
const redirectTo = (to) => {
|
|
||||||
if (uiConfig.value.isAuthModals) {
|
|
||||||
appStore.setActiveAuthState(to);
|
|
||||||
} else {
|
|
||||||
navigateTo(localePath(`/auth/${to}`));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
|
||||||
return (
|
|
||||||
isEmail(email.value) === true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { resetPassword, loading } = usePasswordReset();
|
|
||||||
|
|
||||||
async function handleReset() {
|
|
||||||
await resetPassword(email.value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.form {
|
|
||||||
width: 450px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 30px;
|
|
||||||
|
|
||||||
&__top {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
color: $primary;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subtitle {
|
|
||||||
text-align: center;
|
|
||||||
color: $text;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__main {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
|
|
||||||
&-block {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
<template>
|
|
||||||
<form class="form" @submit.prevent="handleUpdate">
|
|
||||||
<div class="form__box">
|
|
||||||
<ui-input
|
|
||||||
:type="'text'"
|
|
||||||
:placeholder="t('fields.firstName')"
|
|
||||||
:label="t('fields.firstName')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="firstName"
|
|
||||||
/>
|
|
||||||
<ui-input
|
|
||||||
:type="'text'"
|
|
||||||
:placeholder="t('fields.lastName')"
|
|
||||||
:label="t('fields.lastName')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="lastName"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form__box">
|
|
||||||
<ui-input
|
|
||||||
:type="'email'"
|
|
||||||
:placeholder="t('fields.email')"
|
|
||||||
:label="t('fields.email')"
|
|
||||||
:rules="[isEmail]"
|
|
||||||
v-model="email"
|
|
||||||
/>
|
|
||||||
<ui-input
|
|
||||||
:type="'text'"
|
|
||||||
:placeholder="t('fields.phoneNumber')"
|
|
||||||
:label="t('fields.phoneNumber')"
|
|
||||||
:rules="[required]"
|
|
||||||
v-model="phoneNumber"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form__box">
|
|
||||||
<ui-input
|
|
||||||
:type="'password'"
|
|
||||||
:placeholder="t('fields.newPassword')"
|
|
||||||
:label="t('fields.newPassword')"
|
|
||||||
:rules="[isPasswordValid]"
|
|
||||||
v-model="password"
|
|
||||||
/>
|
|
||||||
<ui-input
|
|
||||||
:type="'password'"
|
|
||||||
:placeholder="t('fields.confirmNewPassword')"
|
|
||||||
:label="t('fields.confirmNewPassword')"
|
|
||||||
:rules="[compareStrings]"
|
|
||||||
v-model="confirmPassword"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ui-button
|
|
||||||
:type="'submit'"
|
|
||||||
class="form__button"
|
|
||||||
:isLoading="loading"
|
|
||||||
>
|
|
||||||
{{ t('buttons.saveChanges') }}
|
|
||||||
</ui-button>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useValidators} from '@composables/rules';
|
|
||||||
import {useUserUpdating} from '@composables/user';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
const { required, isEmail, isPasswordValid } = useValidators();
|
|
||||||
|
|
||||||
const user = computed(() => userStore.user);
|
|
||||||
|
|
||||||
const firstName = ref<string>('');
|
|
||||||
const lastName = ref<string>('');
|
|
||||||
const email = ref<string>('');
|
|
||||||
const phoneNumber = ref<string>('');
|
|
||||||
const password = ref<string>('');
|
|
||||||
const confirmPassword = ref<string>('');
|
|
||||||
|
|
||||||
const compareStrings = (v: string) => {
|
|
||||||
if (v === password.value) return true;
|
|
||||||
return t('errors.compare');
|
|
||||||
};
|
|
||||||
|
|
||||||
const { updateUser, loading } = useUserUpdating();
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
firstName.value = user.value?.firstName || '';
|
|
||||||
lastName.value = user.value?.lastName || '';
|
|
||||||
email.value = user.value?.email || '';
|
|
||||||
phoneNumber.value = user.value?.phoneNumber || '';
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleUpdate() {
|
|
||||||
await updateUser(
|
|
||||||
firstName.value,
|
|
||||||
lastName.value,
|
|
||||||
email.value,
|
|
||||||
phoneNumber.value,
|
|
||||||
password.value,
|
|
||||||
confirmPassword.value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
&__box {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
width: fit-content;
|
|
||||||
padding-inline: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="ad">
|
|
||||||
<div class="container">
|
|
||||||
<div class="ad__wrapper">
|
|
||||||
<div class="ad__block">
|
|
||||||
<h2 class="ad__title">{{ t('home.ad.title') }}</h2>
|
|
||||||
<p class="ad__subtext">{{ t('home.ad.text1') }}</p>
|
|
||||||
<p class="ad__text">{{ t('home.ad.text2') }}</p>
|
|
||||||
<nuxt-link-locale to="/shop" class="ad__button">{{ t('buttons.shopTheSale') }}</nuxt-link-locale>
|
|
||||||
</div>
|
|
||||||
<nuxt-img
|
|
||||||
format="webp"
|
|
||||||
densities="x1"
|
|
||||||
src="/images/saleImage.png"
|
|
||||||
alt="ad"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const {t} = useI18n();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.ad {
|
|
||||||
&__wrapper {
|
|
||||||
background: $primary_gradient;
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
padding: 32px 32px 32px 50px;
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__block {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: $main;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-size: 36px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subtext {
|
|
||||||
color: $main;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
color: $border;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
margin-top: 20px;
|
|
||||||
width: fit-content;
|
|
||||||
background-color: $main;
|
|
||||||
padding: 15px 35px;
|
|
||||||
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: $primary_dark;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.1px;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $primary_dark;
|
|
||||||
color: $main;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="blog">
|
|
||||||
<div class="container">
|
|
||||||
<div class="blog__wrapper">
|
|
||||||
<h2 class="blog__title">{{ t('home.blog.title') }}</h2>
|
|
||||||
<div class="blog__posts">
|
|
||||||
<cards-post
|
|
||||||
v-for="post in filteredPosts.slice(0, 3)"
|
|
||||||
:key="post.node.id"
|
|
||||||
:post="post.node"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type {IPost} from '@types';
|
|
||||||
import { docsSlugs } from '@appConstants';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
posts: { node: IPost }[];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const {t} = useI18n();
|
|
||||||
|
|
||||||
const filteredPosts = computed(() => {
|
|
||||||
const excludedSlugs = Object.values(docsSlugs);
|
|
||||||
return props.posts?.filter(post => !excludedSlugs.includes(post.node.slug));
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.blog {
|
|
||||||
&__wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
color: $primary_dark;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__posts {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="brands">
|
|
||||||
<nuxt-marquee
|
|
||||||
class="brands__marquee"
|
|
||||||
id="marquee-slider"
|
|
||||||
:speed="50"
|
|
||||||
:pauseOnHover="true"
|
|
||||||
>
|
|
||||||
<nuxt-link-locale
|
|
||||||
class="brands__item"
|
|
||||||
v-for="brand in brands"
|
|
||||||
:key="brand.node.uuid"
|
|
||||||
:to="`/brand/${brand.node.slug}`"
|
|
||||||
>
|
|
||||||
<p>{{ brand.node.name }}</p>
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</nuxt-marquee>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type {IBrand} from '@types';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
brands: { node: IBrand }[];
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.brands {
|
|
||||||
background-color: $border;
|
|
||||||
padding-block: 65px;
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
margin-right: 65px;
|
|
||||||
color: $text;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: 1.9px;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
text-shadow: 0 0 10px $primary_dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="categories">
|
|
||||||
<div class="container">
|
|
||||||
<div class="categories__wrapper">
|
|
||||||
<h2 class="categories__title">{{ t('home.categories.title') }}</h2>
|
|
||||||
<swiper
|
|
||||||
class="swiper"
|
|
||||||
:modules="[Pagination]"
|
|
||||||
:spaceBetween="24"
|
|
||||||
:breakpoints="{
|
|
||||||
200: {
|
|
||||||
slidesPerView: 3
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<swiper-slide
|
|
||||||
class="swiper__slide"
|
|
||||||
v-for="category in categories"
|
|
||||||
:key="category.node.uuid"
|
|
||||||
>
|
|
||||||
<nuxt-link-locale :to="`/catalog/${category.node.slug}`">
|
|
||||||
<nuxt-img
|
|
||||||
format="webp"
|
|
||||||
densities="x1"
|
|
||||||
:src="category.node.image"
|
|
||||||
:alt="category.node.name"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p>{{ category.node.name }}</p>
|
|
||||||
</div>
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</swiper-slide>
|
|
||||||
</swiper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {Pagination} from "swiper/modules";
|
|
||||||
import {Swiper, SwiperSlide} from "swiper/vue";
|
|
||||||
|
|
||||||
const {t} = useI18n();
|
|
||||||
const categoryStore = useCategoryStore();
|
|
||||||
|
|
||||||
const categories = computed(() => categoryStore.categories);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.categories {
|
|
||||||
&__wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
color: $primary_dark;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&__slide {
|
|
||||||
& a {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
& img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
& div {
|
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
|
||||||
padding: 25px;
|
|
||||||
|
|
||||||
& p {
|
|
||||||
color: $secondary;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="hero">
|
|
||||||
<div class="container">
|
|
||||||
<div class="hero__wrapper">
|
|
||||||
<h2 class="hero__title">{{ t('home.hero.title') }}</h2>
|
|
||||||
<p class="hero__text">{{ t('home.hero.text') }}</p>
|
|
||||||
<nuxt-link-locale to="/shop" class="hero__button">{{ t('buttons.shopNow') }}</nuxt-link-locale>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const {t} = useI18n();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.hero {
|
|
||||||
position: relative;
|
|
||||||
background-image: url(/images/heroImage.png);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
-webkit-background-size: cover;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: top;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: $blackout;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
padding-block: 185px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 24px;
|
|
||||||
width: 675px;
|
|
||||||
margin-inline: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
color: $main;
|
|
||||||
text-align: center;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-size: 60px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
text-align: center;
|
|
||||||
color: $main;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 300;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
margin-top: 10px;
|
|
||||||
background-color: $main;
|
|
||||||
padding: 10px 35px;
|
|
||||||
|
|
||||||
color: $primary_dark;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.1px;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $primary_dark;
|
|
||||||
color: $main;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
<template>
|
|
||||||
<nav class="nav">
|
|
||||||
<div class="nav__inner">
|
|
||||||
<nuxt-link-locale
|
|
||||||
class="nav__item"
|
|
||||||
:class="[{ active: route.path.includes('settings') }]"
|
|
||||||
to="/profile/settings"
|
|
||||||
>
|
|
||||||
<icon name="ic:baseline-settings" size="20" />
|
|
||||||
{{ t('profile.settings.title') }}
|
|
||||||
</nuxt-link-locale>
|
|
||||||
<nuxt-link-locale
|
|
||||||
class="nav__item"
|
|
||||||
:class="[{ active: route.path.includes('orders') }]"
|
|
||||||
to="/profile/orders"
|
|
||||||
>
|
|
||||||
<icon name="material-symbols:order-approve-rounded" size="20" />
|
|
||||||
{{ t('profile.orders.title') }}
|
|
||||||
</nuxt-link-locale>
|
|
||||||
<!-- <nuxt-link-locale-->
|
|
||||||
<!-- class="nav__item"-->
|
|
||||||
<!-- :class="[{ active: route.path.includes('balance') }]"-->
|
|
||||||
<!-- to="/profile/balance"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <icon name="ic:outline-attach-money" size="20" />-->
|
|
||||||
<!-- {{ t('profile.balance.title') }}-->
|
|
||||||
<!-- </nuxt-link-locale>-->
|
|
||||||
<nuxt-link-locale
|
|
||||||
class="nav__item"
|
|
||||||
:class="[{ active: route.path.includes('promocodes') }]"
|
|
||||||
to="/profile/promocodes"
|
|
||||||
>
|
|
||||||
<icon name="fluent:ticket-20-filled" size="20" />
|
|
||||||
{{ t('profile.promocodes.title') }}
|
|
||||||
</nuxt-link-locale>
|
|
||||||
</div>
|
|
||||||
<div class="nav__logout" @click="logout">
|
|
||||||
<icon name="material-symbols:power-settings-new-outline" size="20" />
|
|
||||||
{{ t('profile.logout') }}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useLogout} from '@composables/auth';
|
|
||||||
|
|
||||||
const {t} = useI18n();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const { logout } = useLogout();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.nav {
|
|
||||||
position: sticky;
|
|
||||||
top: 116px;
|
|
||||||
width: 256px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
height: fit-content;
|
|
||||||
|
|
||||||
&__inner {
|
|
||||||
background-color: $main;
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border: 1px solid $border;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-right: 2px solid transparent;
|
|
||||||
border-bottom: 1px solid $border;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
color: $text;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $primary;
|
|
||||||
background-color: $main_hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-right-color: $primary;
|
|
||||||
color: $primary;
|
|
||||||
background-color: $main_hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__logout {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 25px;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
background-color: rgba($primary, 0.2);
|
|
||||||
border: 1px solid $primary;
|
|
||||||
padding: 7px 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
color: $primary;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $primary;
|
|
||||||
color: $main;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
<template>
|
|
||||||
<el-skeleton
|
|
||||||
class="sk"
|
|
||||||
animated
|
|
||||||
>
|
|
||||||
<template #template>
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="image"
|
|
||||||
class="sk__image"
|
|
||||||
/>
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="p"
|
|
||||||
class="sk__name"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</el-skeleton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{
|
|
||||||
isList?: boolean;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.sk {
|
|
||||||
width: 100%;
|
|
||||||
background-color: $skeleton;
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
border: 1px solid $border;
|
|
||||||
padding: 23px;
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
width: 100%;
|
|
||||||
height: 233px;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__name {
|
|
||||||
width: 100%;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
<template>
|
|
||||||
<el-skeleton
|
|
||||||
class="sk"
|
|
||||||
animated
|
|
||||||
>
|
|
||||||
<template #template>
|
|
||||||
<div class="sk__main">
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="p"
|
|
||||||
class="sk__text"
|
|
||||||
/>
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="p"
|
|
||||||
class="sk__text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="sk__bottom">
|
|
||||||
<div class="sk__bottom-images">
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="image"
|
|
||||||
class="sk__image"
|
|
||||||
v-for="idx in 3"
|
|
||||||
:key="idx"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="p"
|
|
||||||
class="sk__price"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-skeleton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.sk {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
background-color: $skeleton;
|
|
||||||
border: 1px solid $primary;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: 10px 8px;
|
|
||||||
|
|
||||||
&__main {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 25px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
width: 100px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__bottom {
|
|
||||||
border-top: 1px solid $primary;
|
|
||||||
padding-top: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
&-images {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
width: 52px;
|
|
||||||
height: 65px;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__price {
|
|
||||||
width: 60px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
<template>
|
|
||||||
<el-skeleton
|
|
||||||
class="sk"
|
|
||||||
:class="[{'sk__list': isList }]"
|
|
||||||
animated
|
|
||||||
>
|
|
||||||
<template #template>
|
|
||||||
<div class="sk__content">
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="image"
|
|
||||||
class="sk__image"
|
|
||||||
/>
|
|
||||||
<div class="sk__content-wrapper">
|
|
||||||
<div class="sk__content-inner">
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="p"
|
|
||||||
class="sk__brand"
|
|
||||||
/>
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="p"
|
|
||||||
class="sk__name"
|
|
||||||
/>
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="p"
|
|
||||||
class="sk__rating"
|
|
||||||
/>
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="p"
|
|
||||||
class="sk__price"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="sk__buttons">
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="p"
|
|
||||||
class="sk__button"
|
|
||||||
/>
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="p"
|
|
||||||
class="sk__button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-skeleton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{
|
|
||||||
isList?: boolean;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.sk {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
border: 1px solid $border;
|
|
||||||
background-color: $skeleton;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&__list {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 15px;
|
|
||||||
|
|
||||||
& .sk__content {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 50px;
|
|
||||||
|
|
||||||
&-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-inner {
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .sk__image {
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .sk__price {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .sk__quantity {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .sk__buttons {
|
|
||||||
width: fit-content;
|
|
||||||
margin-top: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .sk__button {
|
|
||||||
&:first-child {
|
|
||||||
width: 140px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
&-wrapper {
|
|
||||||
padding: 10px 20px 20px 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__price {
|
|
||||||
width: 50px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__name {
|
|
||||||
width: 100%;
|
|
||||||
height: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__rating {
|
|
||||||
margin-block: 4px 10px;
|
|
||||||
width: 120px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__brand {
|
|
||||||
width: 75px;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__buttons {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<template>
|
|
||||||
<el-skeleton class="sk" animated>
|
|
||||||
<template #template>
|
|
||||||
<div class="sk__block" v-for="idx in 3" :key="idx">
|
|
||||||
<el-skeleton-item variant="p" class="sk__title" />
|
|
||||||
<el-skeleton-item variant="p" class="sk__text" />
|
|
||||||
<el-skeleton-item variant="p" class="sk__text" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-skeleton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.sk {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
background-color: $skeleton;
|
|
||||||
|
|
||||||
&__block {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
width: 200px;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
width: 100%;
|
|
||||||
height: 21px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//:deep(.el-skeleton__item) {
|
|
||||||
// --el-skeleton-color: #c9ccd0 !important;
|
|
||||||
// --el-skeleton-to-color: #c3c3c7 !important;
|
|
||||||
//}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<template>
|
|
||||||
<el-skeleton class="sk" animated>
|
|
||||||
<template #template>
|
|
||||||
<el-skeleton-item
|
|
||||||
variant="p"
|
|
||||||
class="sk__text"
|
|
||||||
v-for="idx in 5"
|
|
||||||
:key="idx"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</el-skeleton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.sk {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
width: 100%;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,316 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="filters">
|
|
||||||
<div class="filters__top">
|
|
||||||
<h2>{{ t('store.filters.title') }}</h2>
|
|
||||||
<p @click="resetFilters">{{ t('buttons.clearAll') }}</p>
|
|
||||||
</div>
|
|
||||||
<client-only>
|
|
||||||
<el-collapse v-model="collapse">
|
|
||||||
<el-collapse-item
|
|
||||||
name="0"
|
|
||||||
>
|
|
||||||
<template #title="{ isActive }">
|
|
||||||
<div class="filters__head">
|
|
||||||
<h3 class="filters__name" v-text="t('store.filters.price')" />
|
|
||||||
<icon
|
|
||||||
name="material-symbols:keyboard-arrow-down"
|
|
||||||
size="22"
|
|
||||||
class="filters__head-icon"
|
|
||||||
:class="[{ active: isActive }]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="filters__price">
|
|
||||||
<div class="filters__price-inputs">
|
|
||||||
<ui-input
|
|
||||||
:model-value="priceMinInput"
|
|
||||||
type="text"
|
|
||||||
placeholder="Min"
|
|
||||||
input-mode="decimal"
|
|
||||||
@update:model-value="(val) => priceMinInput = val"
|
|
||||||
@blur="handlePriceBlur(priceMinInput, 'min')"
|
|
||||||
/>
|
|
||||||
<span class="filters__separator">—</span>
|
|
||||||
<ui-input
|
|
||||||
:model-value="priceMaxInput"
|
|
||||||
type="text"
|
|
||||||
placeholder="Max"
|
|
||||||
input-mode="decimal"
|
|
||||||
@update:model-value="(val) => priceMaxInput = val"
|
|
||||||
@blur="handlePriceBlur(priceMaxInput, 'max')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<el-slider
|
|
||||||
v-model="priceRange"
|
|
||||||
:min="categoryMin"
|
|
||||||
:max="categoryMax"
|
|
||||||
range
|
|
||||||
:step="0.01"
|
|
||||||
:format-tooltip="formatPriceTooltip"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-collapse-item>
|
|
||||||
<el-collapse-item
|
|
||||||
v-if="filterableAttributes"
|
|
||||||
v-for="(attribute, idx) in filterableAttributes"
|
|
||||||
:key="idx"
|
|
||||||
:name="`${idx + 2}`"
|
|
||||||
>
|
|
||||||
<template #title="{ isActive }">
|
|
||||||
<div class="filters__head">
|
|
||||||
<h3 class="filters__name" v-text="attribute.attributeName" />
|
|
||||||
<icon
|
|
||||||
name="material-symbols:keyboard-arrow-down"
|
|
||||||
size="22"
|
|
||||||
class="filters__head-icon"
|
|
||||||
:class="[{ active: isActive }]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<ul class="filters__list">
|
|
||||||
<li
|
|
||||||
v-for="(value, idx) of attribute.possibleValues"
|
|
||||||
:key="idx"
|
|
||||||
class="filters__item"
|
|
||||||
>
|
|
||||||
<ui-checkbox
|
|
||||||
:id="attribute.attributeName + idx"
|
|
||||||
v-model="selectedMap[attribute.attributeName][value]"
|
|
||||||
>
|
|
||||||
{{ value }}
|
|
||||||
</ui-checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</el-collapse-item>
|
|
||||||
</el-collapse>
|
|
||||||
</client-only>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type {IStoreFilters} from '@types';
|
|
||||||
import {useFilters} from '@composables/store';
|
|
||||||
import {useRouteQuery} from '@vueuse/router';
|
|
||||||
import {CURRENCY} from "~/constants";
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
filterableAttributes?: IStoreFilters[];
|
|
||||||
initialMinPrice?: number;
|
|
||||||
initialMaxPrice?: number;
|
|
||||||
categoryMinPrice?: number;
|
|
||||||
categoryMaxPrice?: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits(["filterMinPrice", "filterMaxPrice", "update:selected"]);
|
|
||||||
|
|
||||||
const attributesQuery = useRouteQuery<string>('attributes', '');
|
|
||||||
|
|
||||||
const {
|
|
||||||
selectedMap,
|
|
||||||
selectedAllMap,
|
|
||||||
priceRange,
|
|
||||||
collapse,
|
|
||||||
toggleAll,
|
|
||||||
resetFilters,
|
|
||||||
applyFilters,
|
|
||||||
parseAttributesString
|
|
||||||
} = useFilters(
|
|
||||||
toRef(props, 'filterableAttributes')
|
|
||||||
);
|
|
||||||
|
|
||||||
const priceMinInput = computed({
|
|
||||||
get: () => String(priceRange.value[0]),
|
|
||||||
set: (val: string | number) => {
|
|
||||||
handlePriceInput(val, 'min');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const priceMaxInput = computed({
|
|
||||||
get: () => String(priceRange.value[1]),
|
|
||||||
set: (val: string | number) => {
|
|
||||||
handlePriceInput(val, 'max');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const categoryMin = computed(() => props.categoryMinPrice ?? 0);
|
|
||||||
const categoryMax = computed(() => props.categoryMaxPrice ?? 50000);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initializeInputs();
|
|
||||||
});
|
|
||||||
|
|
||||||
const initializeInputs = () => {
|
|
||||||
const min = props.initialMinPrice ?? categoryMin.value;
|
|
||||||
const max = props.initialMaxPrice ?? categoryMax.value;
|
|
||||||
|
|
||||||
priceRange.value = [min, max];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePriceInput = useDebounceFn((value: string | number, type: 'min' | 'max') => {
|
|
||||||
const strValue = String(value).replace(',', '.');
|
|
||||||
const numValue = parseFloat(strValue);
|
|
||||||
|
|
||||||
if (isNaN(numValue)) return;
|
|
||||||
|
|
||||||
if (type === 'min') {
|
|
||||||
const clamped = Math.max(categoryMin.value, Math.min(numValue, priceRange.value[1]));
|
|
||||||
priceRange.value = [clamped, priceRange.value[1]];
|
|
||||||
} else {
|
|
||||||
const clamped = Math.max(priceRange.value[0], Math.min(numValue, categoryMax.value));
|
|
||||||
priceRange.value = [priceRange.value[0], clamped];
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
const handlePriceBlur = (value: string | number, type: 'min' | 'max') => {
|
|
||||||
const strValue = String(value).trim();
|
|
||||||
|
|
||||||
if (strValue === '') {
|
|
||||||
if (type === 'min') {
|
|
||||||
priceRange.value = [categoryMin.value, priceRange.value[1]];
|
|
||||||
} else {
|
|
||||||
priceRange.value = [priceRange.value[0], categoryMax.value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedPriceUpdate = useDebounceFn(() => {
|
|
||||||
emit("filterMinPrice", priceRange.value[0]);
|
|
||||||
emit("filterMaxPrice", priceRange.value[1]);
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
const debouncedFilterApply = useDebounceFn(() => {
|
|
||||||
const picked = applyFilters();
|
|
||||||
emit('update:selected', picked);
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
watch(priceRange, () => {
|
|
||||||
debouncedPriceUpdate();
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
watch(selectedMap, () => {
|
|
||||||
debouncedFilterApply();
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [props.categoryMinPrice, props.categoryMaxPrice, props.initialMinPrice, props.initialMaxPrice],
|
|
||||||
([catMin, catMax, initMin, initMax]) => {
|
|
||||||
const min = initMin ?? catMin ?? 0;
|
|
||||||
const max = initMax ?? catMax ?? 50000;
|
|
||||||
|
|
||||||
priceRange.value = [min, max];
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => attributesQuery.value,
|
|
||||||
(attrStr) => {
|
|
||||||
const initial = parseAttributesString(attrStr);
|
|
||||||
const hasFloatInQuery = initial['float'] && initial['float'].length === 2;
|
|
||||||
|
|
||||||
if (!hasFloatInQuery) {
|
|
||||||
resetFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!attrStr) return;
|
|
||||||
|
|
||||||
Object.entries(initial).forEach(([key, vals]) => {
|
|
||||||
if (key === 'float' && vals.length === 2) {
|
|
||||||
const min = parseFloat(vals[0]);
|
|
||||||
const max = parseFloat(vals[1]);
|
|
||||||
floatRange.value = [min, max];
|
|
||||||
} else {
|
|
||||||
vals.forEach(val => {
|
|
||||||
if (selectedMap[key] && selectedMap[key][val] !== undefined) {
|
|
||||||
selectedMap[key][val] = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (selectedMap[key]) {
|
|
||||||
selectedAllMap[key] = Object.values(selectedMap[key]).every(v => v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatPriceTooltip = (value: number) => `${CURRENCY}${value.toFixed(2)}`;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.filters {
|
|
||||||
width: 290px;
|
|
||||||
border: 1px solid $border;
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
&__top {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
& h2 {
|
|
||||||
color: $primary_dark;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& p {
|
|
||||||
cursor: pointer;
|
|
||||||
color: $link_primary;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $link_primary_hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
&-icon {
|
|
||||||
color: $secondary;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
transform: rotate(-180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__list {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__name {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: $primary_dark;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__price {
|
|
||||||
&-inputs {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.block__input) {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,226 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="store">
|
|
||||||
<store-filter
|
|
||||||
v-if="filters.length"
|
|
||||||
:filterableAttributes="filters"
|
|
||||||
@update:selected="onFiltersChange"
|
|
||||||
:initialMinPrice="Number(minPrice)"
|
|
||||||
:initialMaxPrice="Number(maxPrice)"
|
|
||||||
:categoryMinPrice="minMaxPrices.minPrice"
|
|
||||||
:categoryMaxPrice="minMaxPrices.maxPrice"
|
|
||||||
@filterMinPrice="updateMinPrice"
|
|
||||||
@filterMaxPrice="updateMaxPrice"
|
|
||||||
/>
|
|
||||||
<div class="store__inner">
|
|
||||||
<store-top
|
|
||||||
v-model="orderBy"
|
|
||||||
@toggle-filter="onFilterToggle"
|
|
||||||
:isFilters="filters.length > 0"
|
|
||||||
/>
|
|
||||||
<client-only>
|
|
||||||
<div
|
|
||||||
class="store__list"
|
|
||||||
:class="[
|
|
||||||
{ 'store__list-grid': productView === 'grid' },
|
|
||||||
{ 'store__list-list': productView === 'list' }
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<cards-product
|
|
||||||
v-if="products.length && !pending"
|
|
||||||
v-for="product in products"
|
|
||||||
:key="product.node.uuid"
|
|
||||||
:product="product.node"
|
|
||||||
:isList="productView === 'list'"
|
|
||||||
/>
|
|
||||||
<skeletons-cards-product
|
|
||||||
v-if="pending"
|
|
||||||
v-for="idx in 12"
|
|
||||||
:key="idx"
|
|
||||||
:isList="productView === 'list'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</client-only>
|
|
||||||
<div class="store__list-observer" ref="observer"></div>
|
|
||||||
<p class="store__empty" v-if="!products.length && !pending">{{ t('store.empty') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useFilters, useStore} from '@composables/store';
|
|
||||||
import {useCategoryBySlug} from '@composables/categories';
|
|
||||||
import {useBrandBySlug} from '@composables/brands';
|
|
||||||
import {useDefaultSeo} from '@composables/seo';
|
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
|
||||||
const { $appHelpers } = useNuxtApp();
|
|
||||||
|
|
||||||
const productView = useCookie<string>(
|
|
||||||
$appHelpers.COOKIES_PRODUCT_VIEW_KEY as string,
|
|
||||||
{
|
|
||||||
default: () => 'grid',
|
|
||||||
path: '/',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const categorySlug = useRouteParams<string>('categorySlug');
|
|
||||||
const brandSlug = useRouteParams<number>('brandSlug');
|
|
||||||
const attributes = useRouteQuery<string>('attributes', '');
|
|
||||||
const orderBy = useRouteQuery<string>('orderBy', 'created');
|
|
||||||
const minPrice = useRouteQuery<number>('minPrice', 0);
|
|
||||||
const maxPrice = useRouteQuery<number>('maxPrice', 50000);
|
|
||||||
const observer = ref(null);
|
|
||||||
|
|
||||||
const categoryData = categorySlug.value
|
|
||||||
? await useCategoryBySlug(categorySlug.value)
|
|
||||||
: { category: ref(null), seoMeta: ref(null), filters: ref([]) };
|
|
||||||
|
|
||||||
const brandData = brandSlug.value
|
|
||||||
? await useBrandBySlug(brandSlug.value)
|
|
||||||
: { brand: ref(null), seoMeta: ref(null) };
|
|
||||||
|
|
||||||
const { category, seoMeta: categorySeoMeta, filters, minMaxPrices } = categoryData;
|
|
||||||
const { brand, seoMeta: brandSeoMeta } = brandData;
|
|
||||||
|
|
||||||
const seoMeta = computed(() => categorySeoMeta.value || brandSeoMeta.value);
|
|
||||||
|
|
||||||
const meta = useDefaultSeo(seoMeta.value || null);
|
|
||||||
|
|
||||||
if (meta) {
|
|
||||||
useSeoMeta({
|
|
||||||
title: meta.title || $appHelpers.APP_NAME,
|
|
||||||
description: meta.description || meta.title || $appHelpers.APP_NAME,
|
|
||||||
ogTitle: meta.og.title || undefined,
|
|
||||||
ogDescription: meta.og.description || meta.title || $appHelpers.APP_NAME,
|
|
||||||
ogType: meta.og.type || undefined,
|
|
||||||
ogUrl: meta.og.url || undefined,
|
|
||||||
ogImage: meta.og.image || undefined,
|
|
||||||
twitterCard: meta.twitter.card || undefined,
|
|
||||||
twitterTitle: meta.twitter.title || undefined,
|
|
||||||
twitterDescription: meta.twitter.description || undefined,
|
|
||||||
robots: meta.robots,
|
|
||||||
});
|
|
||||||
|
|
||||||
useHead({
|
|
||||||
link: [
|
|
||||||
meta.canonical ? { rel: 'canonical', href: meta.canonical } : {},
|
|
||||||
].filter(Boolean) as any,
|
|
||||||
meta: [{ property: 'og:locale', content: locale.value }],
|
|
||||||
script: meta.jsonLd.map((obj: any) => ({
|
|
||||||
type: 'application/ld+json',
|
|
||||||
innerHTML: JSON.stringify(obj),
|
|
||||||
})),
|
|
||||||
__dangerouslyDisableSanitizersByTagID: Object.fromEntries(
|
|
||||||
meta.jsonLd.map((_, i: number) => [`ldjson-${i}`, ['innerHTML']])
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => category.value,
|
|
||||||
(cat) => {
|
|
||||||
if (cat && !useRoute().query.maxPrice) {
|
|
||||||
maxPrice.value = cat.minMaxPrices.maxPrice;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const { pending, products, pageInfo, variables, getProducts } = useStore({
|
|
||||||
orderBy: orderBy.value,
|
|
||||||
categoriesSlugs: categorySlug.value,
|
|
||||||
productAfter: '',
|
|
||||||
minPrice: minPrice.value,
|
|
||||||
maxPrice: maxPrice.value,
|
|
||||||
brand: brand.value?.name,
|
|
||||||
attributes: attributes.value
|
|
||||||
});
|
|
||||||
await getProducts();
|
|
||||||
|
|
||||||
const { buildAttributesString } = useFilters(filters);
|
|
||||||
const showFilter = ref<boolean>(false);
|
|
||||||
|
|
||||||
function onFilterToggle() {
|
|
||||||
showFilter.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onFiltersChange(newFilters: Record<string, string[]>) {
|
|
||||||
const attrString = buildAttributesString(newFilters);
|
|
||||||
attributes.value = attrString;
|
|
||||||
variables.attributes = attrString;
|
|
||||||
await getProducts();
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateMinPrice = useDebounceFn((filteredPrice) => {
|
|
||||||
minPrice.value = filteredPrice;
|
|
||||||
}, 500);
|
|
||||||
const updateMaxPrice = useDebounceFn((filteredPrice) => {
|
|
||||||
maxPrice.value = filteredPrice;
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
useIntersectionObserver(
|
|
||||||
observer,
|
|
||||||
async ([{ isIntersecting }]) => {
|
|
||||||
if (isIntersecting && pageInfo.value?.hasNextPage && !pending.value) {
|
|
||||||
variables.productAfter = pageInfo.value.endCursor;
|
|
||||||
await getProducts();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
[orderBy, attributes, minPrice, maxPrice],
|
|
||||||
async ([newOrder, newAttr, newMin, newMax]) => {
|
|
||||||
variables.orderBy = newOrder || '';
|
|
||||||
variables.attributes = newAttr;
|
|
||||||
variables.minPrice = Number(newMin) || 0;
|
|
||||||
variables.maxPrice = Number(newMax) || 500000;
|
|
||||||
variables.productAfter = '';
|
|
||||||
products.value = [];
|
|
||||||
|
|
||||||
await getProducts();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.store {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 32px;
|
|
||||||
|
|
||||||
&__inner {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__list {
|
|
||||||
margin-top: 32px;
|
|
||||||
|
|
||||||
&-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
gap: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-observer {
|
|
||||||
background-color: transparent;
|
|
||||||
width: 100%;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__empty {
|
|
||||||
color: $primary;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="top" :class="[{ filters: isFilters }]">
|
|
||||||
<div class="top__sorting">
|
|
||||||
<p>{{ t('store.sorting') }}</p>
|
|
||||||
<client-only>
|
|
||||||
<el-select
|
|
||||||
v-model="select"
|
|
||||||
size="large"
|
|
||||||
style="width: 240px"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in options"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</client-only>
|
|
||||||
</div>
|
|
||||||
<div class="top__view">
|
|
||||||
<button
|
|
||||||
class="top__view-button"
|
|
||||||
:class="{ active: productView === 'list' }"
|
|
||||||
@click="setView('list')"
|
|
||||||
>
|
|
||||||
<icon name="material-symbols:view-list-sharp" size="16" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="top__view-button"
|
|
||||||
:class="{ active: productView === 'grid' }"
|
|
||||||
@click="setView('grid')"
|
|
||||||
>
|
|
||||||
<icon name="material-symbols:grid-view" size="16" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const {t} = useI18n();
|
|
||||||
const { $appHelpers } = useNuxtApp();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: string;
|
|
||||||
isFilters: boolean;
|
|
||||||
}>();
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: string): void;
|
|
||||||
(e: 'toggle-filter'): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const productView = useCookie($appHelpers.COOKIES_PRODUCT_VIEW_KEY as string);
|
|
||||||
function setView(view: 'list' | 'grid') {
|
|
||||||
productView.value = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
const select = ref(props.modelValue || 'created');
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
value: 'created',
|
|
||||||
label: 'New',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'rating',
|
|
||||||
label: 'Rating',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'price',
|
|
||||||
label: 'Сheap first',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: '-price',
|
|
||||||
label: 'Expensive first',
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
watch(select, value => {
|
|
||||||
emit('update:modelValue', value);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.top {
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 40px;
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
border: 1px solid $border;
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
&.filters {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__sorting {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
& p {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 14px;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
color: $text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__view {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
border: 1px solid $border;
|
|
||||||
|
|
||||||
&-button {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: $main;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
color: $primary_dark;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $main_hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: $link_secondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
<template>
|
|
||||||
<client-only>
|
|
||||||
<el-breadcrumb separator="/" class="breadcrumbs">
|
|
||||||
<el-breadcrumb-item
|
|
||||||
v-for="(crumb, idx) in breadcrumbs"
|
|
||||||
:key="idx"
|
|
||||||
>
|
|
||||||
<nuxt-link-locale
|
|
||||||
v-if="idx !== breadcrumbs.length - 1"
|
|
||||||
:to="crumb.link"
|
|
||||||
class="breadcrumbs__link"
|
|
||||||
>
|
|
||||||
{{ crumb.text }}
|
|
||||||
</nuxt-link-locale>
|
|
||||||
<span v-else class="breadcrumbs__current">
|
|
||||||
{{ crumb.text }}
|
|
||||||
</span>
|
|
||||||
</el-breadcrumb-item>
|
|
||||||
</el-breadcrumb>
|
|
||||||
</client-only>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useBreadcrumbs} from '@composables/breadcrumbs';
|
|
||||||
|
|
||||||
const { breadcrumbs } = useBreadcrumbs();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.breadcrumbs {
|
|
||||||
background-color: $title_bg;
|
|
||||||
padding: 15px 250px 15px 50px;
|
|
||||||
line-height: 140%;
|
|
||||||
border-bottom: 1px solid $border;
|
|
||||||
|
|
||||||
&__link {
|
|
||||||
cursor: pointer !important;
|
|
||||||
color: $primary !important;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $primary_dark !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__current {
|
|
||||||
font-weight: 600;
|
|
||||||
color: $primary_dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
class="button"
|
|
||||||
:disabled="isDisabled"
|
|
||||||
:class="[
|
|
||||||
{ active: isLoading },
|
|
||||||
{ secondary: style === 'secondary' }
|
|
||||||
]"
|
|
||||||
:type="type"
|
|
||||||
>
|
|
||||||
<ui-loader class="button__loader" v-if="isLoading" />
|
|
||||||
<slot v-else />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{
|
|
||||||
type: 'submit' | 'button';
|
|
||||||
isDisabled?: boolean;
|
|
||||||
isLoading?: boolean;
|
|
||||||
style?: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.button {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background-color: $primary;
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
padding-block: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
color: $main;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
&.secondary {
|
|
||||||
background-color: $main;
|
|
||||||
border: 1px solid $border;
|
|
||||||
color: $secondary;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $border;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: $border;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
background-color: $disabled_secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled:hover, &.active {
|
|
||||||
background-color: $disabled_secondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $primary_hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: $primary_hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
background-color: $disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled:hover, &.active {
|
|
||||||
background-color: $disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__loader {
|
|
||||||
margin-block: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
<template>
|
|
||||||
<label class="checkbox" :class="{ isPrimary }">
|
|
||||||
<input
|
|
||||||
:id="id"
|
|
||||||
class="checkbox__input"
|
|
||||||
type="checkbox"
|
|
||||||
:checked="modelValue"
|
|
||||||
@change="onChange"
|
|
||||||
/>
|
|
||||||
<span class="checkbox__block"></span>
|
|
||||||
<span class="checkbox__label">
|
|
||||||
<slot/>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{
|
|
||||||
id?: string;
|
|
||||||
modelValue: boolean;
|
|
||||||
isPrimary?: boolean;
|
|
||||||
}>();
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', v: boolean): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
function onChange(e: Event) {
|
|
||||||
const checked = (e.target as HTMLInputElement).checked;
|
|
||||||
emit('update:modelValue', checked);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.isPrimary {
|
|
||||||
& .checkbox__block {
|
|
||||||
border: 2px solid $primary_dark;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .checkbox__label {
|
|
||||||
color: $primary_dark;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__input {
|
|
||||||
display: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__block {
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 0.5px solid $primary_dark;
|
|
||||||
border-radius: 1px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
background-color: $primary_dark;
|
|
||||||
border-radius: 2px;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
cursor: pointer;
|
|
||||||
color: $secondary;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox__input:checked + .checkbox__block::after {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="block">
|
|
||||||
<div class="block__wrapper">
|
|
||||||
<label v-if="label" class="block__label">{{ label }}</label>
|
|
||||||
<div class="block__wrapper-inner">
|
|
||||||
<input
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:type="isPasswordVisible"
|
|
||||||
:value="modelValue"
|
|
||||||
@input="onInput"
|
|
||||||
@keydown="numberOnly ? onlyNumbersKeydown($event) : null"
|
|
||||||
class="block__input"
|
|
||||||
:inputmode="inputMode || 'text'"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
@click.prevent="togglePasswordVisible"
|
|
||||||
class="block__eyes"
|
|
||||||
v-if="type === 'password' && String(modelValue).length > 0"
|
|
||||||
>
|
|
||||||
<icon v-if="isPasswordVisible === 'password'" name="mdi:eye-off-outline" />
|
|
||||||
<icon v-else name="mdi:eye-outline" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-if="!isValid" class="block__error">{{ errorMessage }}</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
type Rule = (value: string) => boolean | string;
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: string | number): void;
|
|
||||||
}>();
|
|
||||||
const props = defineProps<{
|
|
||||||
type: string;
|
|
||||||
placeholder: string;
|
|
||||||
modelValue?: string | number;
|
|
||||||
rules?: Rule[];
|
|
||||||
label?: string;
|
|
||||||
numberOnly?: boolean;
|
|
||||||
inputMode?: "text" | "email" | "search" | "tel" | "url" | "none" | "numeric" | "decimal";
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const isPasswordVisible = ref<string>(props.type);
|
|
||||||
const isValid = ref<boolean>(true);
|
|
||||||
const errorMessage = ref<string>('');
|
|
||||||
|
|
||||||
function togglePasswordVisible() {
|
|
||||||
isPasswordVisible.value =
|
|
||||||
isPasswordVisible.value === 'password' ? 'text' : 'password';
|
|
||||||
}
|
|
||||||
|
|
||||||
const onlyNumbersKeydown = (event: KeyboardEvent) => {
|
|
||||||
if (!/^\d$/.test(event.key) &&
|
|
||||||
!['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'Tab', 'Home', 'End'].includes(event.key)) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function onInput(e: Event) {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
let value = target.value;
|
|
||||||
|
|
||||||
if (props.numberOnly) {
|
|
||||||
const digitsOnly = value.replace(/\D/g, '');
|
|
||||||
if (digitsOnly !== value) {
|
|
||||||
target.value = digitsOnly;
|
|
||||||
value = digitsOnly;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let valid = true;
|
|
||||||
errorMessage.value = '';
|
|
||||||
|
|
||||||
props.rules?.forEach((rule) => {
|
|
||||||
const result = rule(value);
|
|
||||||
if (result !== true) {
|
|
||||||
valid = false;
|
|
||||||
errorMessage.value = String(result);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
isValid.value = valid;
|
|
||||||
|
|
||||||
emit('update:modelValue', props.numberOnly ? Number(value) : value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.block {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
&-inner {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
color: $secondary;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 14px 12px;
|
|
||||||
border: 1px solid $border;
|
|
||||||
border-radius: $default_border_radius;
|
|
||||||
background-color: $main;
|
|
||||||
|
|
||||||
color: $primary_dark;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: $disabled_secondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__eyes {
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
right: 20px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background-color: transparent;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
font-size: 18px;
|
|
||||||
color: $disabled_secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__error {
|
|
||||||
color: $error;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
animation: fadeInUp 0.3s ease;
|
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="switcher" ref="switcherRef">
|
|
||||||
<div
|
|
||||||
@click="setSwitcherVisible(!isSwitcherVisible)"
|
|
||||||
class="switcher__button"
|
|
||||||
:class="[{ active: isSwitcherVisible }]"
|
|
||||||
>
|
|
||||||
<client-only>
|
|
||||||
<!-- <icon name="fluent:globe-20-filled" size="20" />-->
|
|
||||||
<nuxt-img
|
|
||||||
format="webp"
|
|
||||||
densities="x1"
|
|
||||||
v-if="currentLocale"
|
|
||||||
:src="currentLocale.flag"
|
|
||||||
:alt="currentLocale.code"
|
|
||||||
/>
|
|
||||||
<!-- <skeletons-ui-language-switcher v-else />-->
|
|
||||||
<template #fallback>
|
|
||||||
<!-- <skeletons-ui-language-switcher />-->
|
|
||||||
</template>
|
|
||||||
</client-only>
|
|
||||||
</div>
|
|
||||||
<client-only>
|
|
||||||
<div
|
|
||||||
class="switcher__menu"
|
|
||||||
:class="[{active: isSwitcherVisible}]"
|
|
||||||
>
|
|
||||||
<div class="switcher__menu-wrapper">
|
|
||||||
<nuxt-img
|
|
||||||
class="switcher__menu-button"
|
|
||||||
v-for="locale of locales"
|
|
||||||
:key="locale.code"
|
|
||||||
format="webp"
|
|
||||||
densities="x1"
|
|
||||||
@click="uiSwitchLanguage(locale.code)"
|
|
||||||
:src="locale.flag"
|
|
||||||
:alt="locale.code"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</client-only>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {onClickOutside} from '@vueuse/core';
|
|
||||||
import {useLanguageSwitch} from '@composables/languages';
|
|
||||||
|
|
||||||
const languageStore = useLanguageStore();
|
|
||||||
|
|
||||||
const locales = computed(() => languageStore.languages);
|
|
||||||
const currentLocale = computed(() => languageStore.currentLocale);
|
|
||||||
|
|
||||||
const isSwitcherVisible = ref<boolean>(false);
|
|
||||||
const setSwitcherVisible = (state: boolean) => {
|
|
||||||
isSwitcherVisible.value = state;
|
|
||||||
};
|
|
||||||
|
|
||||||
const switcherRef = ref(null);
|
|
||||||
onClickOutside(switcherRef, () => isSwitcherVisible.value = false);
|
|
||||||
|
|
||||||
const { switchLanguage } = useLanguageSwitch();
|
|
||||||
|
|
||||||
const uiSwitchLanguage = (localeCode: string) => {
|
|
||||||
switchLanguage(localeCode);
|
|
||||||
setSwitcherVisible(false);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.switcher {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
width: 44px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
padding: 5px;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $primary_shadow;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: $primary_shadow;
|
|
||||||
}
|
|
||||||
|
|
||||||
& img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__menu {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 3;
|
|
||||||
top: 110%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 0fr;
|
|
||||||
transition: grid-template-rows 0.2s ease;
|
|
||||||
|
|
||||||
&-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-button {
|
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 3px 5px;
|
|
||||||
background-color: $link_secondary;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
padding-top: 5px;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $link_secondary_hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
<template>
|
|
||||||
<span
|
|
||||||
class="link"
|
|
||||||
:class="{ 'link--clickable': isClickable }"
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
interface Props {
|
|
||||||
routePath?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
const emit = defineEmits<{
|
|
||||||
click: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const isClickable = computed(() => !!props.routePath);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
if (props.routePath) {
|
|
||||||
if (import.meta.client) {
|
|
||||||
router.push(props.routePath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
emit('click');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.link {
|
|
||||||
width: fit-content;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: $primary;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
&--clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
color: $secondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="loader">
|
|
||||||
<span class="loader__dots" id="dot-1"></span>
|
|
||||||
<span class="loader__dots" id="dot-2"></span>
|
|
||||||
<span class="loader__dots" id="dot-3"></span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.loader {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.6em;
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
&__dots {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: $main;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#dot-1 {
|
|
||||||
animation: loader-1 0.6s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loader-1 {
|
|
||||||
50% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-0.3em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#dot-2 {
|
|
||||||
animation: loader-2 0.6s 0.3s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loader-2 {
|
|
||||||
50% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-0.3em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#dot-3 {
|
|
||||||
animation: loader-3 0.6s 0.6s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loader-3 {
|
|
||||||
50% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-0.3em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,297 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="search">
|
|
||||||
<div class="container">
|
|
||||||
<div class="search__inner">
|
|
||||||
<div
|
|
||||||
@click="toggleSearch(true)"
|
|
||||||
class="search__wrapper"
|
|
||||||
:class="[{ active: isSearchActive }]"
|
|
||||||
>
|
|
||||||
<form class="search__form" @submit.prevent="submitSearch">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
v-model="query"
|
|
||||||
:placeholder="t('fields.search')"
|
|
||||||
inputmode="search"
|
|
||||||
/>
|
|
||||||
<div class="search__tools">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="clearSearch"
|
|
||||||
v-if="query"
|
|
||||||
>
|
|
||||||
<icon name="gridicons:cross" size="16" />
|
|
||||||
</button>
|
|
||||||
<div class="search__tools-line" v-if="query"></div>
|
|
||||||
<button type="submit">
|
|
||||||
<icon name="tabler:search" size="16" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="search__results" :class="[{ active: (searchResults && isSearchActive) || loading }]">
|
|
||||||
<skeletons-header-search v-if="loading" />
|
|
||||||
<div
|
|
||||||
class="search__results-inner"
|
|
||||||
v-for="(blocks, category) in filteredSearchResults"
|
|
||||||
:key="category"
|
|
||||||
>
|
|
||||||
<div class="search__results-title">
|
|
||||||
<p>{{ getBlockTitle(category) }}:</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="search__item"
|
|
||||||
v-for="item in blocks"
|
|
||||||
:key="item.uuid"
|
|
||||||
@click.stop="goTo(category, item)"
|
|
||||||
>
|
|
||||||
<div class="search__item-left">
|
|
||||||
<icon name="ic:twotone-search" size="18" />
|
|
||||||
<p>{{ item.name }}</p>
|
|
||||||
</div>
|
|
||||||
<icon name="line-md:external-link" size="18" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="search__results-empty" v-if="!hasResults && query && !loading">
|
|
||||||
<p>{{ t('header.search.empty') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<transition name="opacity" mode="out-in">
|
|
||||||
<div
|
|
||||||
class="search__bg"
|
|
||||||
@click="toggleSearch(false)"
|
|
||||||
v-if="isSearchActive"
|
|
||||||
/>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useSearchUI } from '@composables/search';
|
|
||||||
|
|
||||||
const {t} = useI18n();
|
|
||||||
const router = useRouter();
|
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
const {
|
|
||||||
query,
|
|
||||||
isSearchActive,
|
|
||||||
loading,
|
|
||||||
searchResults,
|
|
||||||
filteredSearchResults,
|
|
||||||
hasResults,
|
|
||||||
getBlockTitle,
|
|
||||||
clearSearch,
|
|
||||||
toggleSearch
|
|
||||||
} = useSearchUI();
|
|
||||||
|
|
||||||
function submitSearch() {
|
|
||||||
if (query.value) {
|
|
||||||
router.push({
|
|
||||||
path: '/search',
|
|
||||||
query: { q: query.value }
|
|
||||||
})
|
|
||||||
|
|
||||||
toggleSearch(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => isSearchActive.value,
|
|
||||||
(state) => {
|
|
||||||
appStore.setOverflowHidden(state);
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
function goTo(category: string, item: any) {
|
|
||||||
let path = "/";
|
|
||||||
|
|
||||||
switch (category) {
|
|
||||||
case "products": {
|
|
||||||
path = `/product/${item.slug}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "categories": {
|
|
||||||
path = `/catalog/${item.slug}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "brands": {
|
|
||||||
path = `/brand/${item.slug}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "posts": {
|
|
||||||
path = "/";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSearch(false);
|
|
||||||
router.push(path);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.search {
|
|
||||||
width: 100%;
|
|
||||||
background-color: $main;
|
|
||||||
|
|
||||||
&__inner {
|
|
||||||
padding-block: 10px;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__bg {
|
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
|
||||||
height: 100vh;
|
|
||||||
left: 0;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
width: 100vw;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
width: 100%;
|
|
||||||
background-color: $border;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: $main;
|
|
||||||
box-shadow: 0 0 0 1px #0000000a,0 4px 4px #0000000a,0 20px 40px #00000014;
|
|
||||||
|
|
||||||
& .search__wrapper {
|
|
||||||
border-radius: $less_border_radius $less_border_radius 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .search__form input {
|
|
||||||
border-radius: $less_border_radius $less_border_radius 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $main;
|
|
||||||
box-shadow: 0 0 0 1px #0000000a,0 4px 4px #0000000a,0 20px 40px #00000014;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__form {
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
& input {
|
|
||||||
background-color: transparent;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding-inline: 20px 150px;
|
|
||||||
border: 1px solid $link_secondary;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
|
|
||||||
color: $secondary;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__tools {
|
|
||||||
position: absolute;
|
|
||||||
right: 20px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
& button {
|
|
||||||
cursor: pointer;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
padding: 5px 12px;
|
|
||||||
border: 1px solid $primary;
|
|
||||||
background-color: transparent;
|
|
||||||
|
|
||||||
font-size: 12px;
|
|
||||||
color: $primary;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $primary;
|
|
||||||
color: $main;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-line {
|
|
||||||
background-color: $primary;
|
|
||||||
height: 15px;
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__results {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
top: 100%;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 0;
|
|
||||||
overflow: auto;
|
|
||||||
background-color: $main;
|
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.0392156863), 0 4px 4px rgba(0, 0, 0, 0.0392156863), 0 20px 40px rgba(0, 0, 0, 0.0784313725);
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
max-height: 40vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-title {
|
|
||||||
background-color: rgba($primary, 0.2);
|
|
||||||
padding: 7px 20px;
|
|
||||||
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-empty {
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 7px 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 30px;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $main_hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& p {
|
|
||||||
word-break: break-all;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
& span {
|
|
||||||
color: $text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="block">
|
|
||||||
<div class="block__inner">
|
|
||||||
<label v-if="label" class="block__label">{{ label }}</label>
|
|
||||||
<textarea
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:value="modelValue"
|
|
||||||
@input="onInput"
|
|
||||||
class="block__textarea"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p v-if="!isValid" class="block__error">{{ errorMessage }}</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
type Rule = (value: string) => boolean | string;
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: string | number): void;
|
|
||||||
}>();
|
|
||||||
const props = defineProps<{
|
|
||||||
placeholder: string;
|
|
||||||
modelValue?: string;
|
|
||||||
rules?: Rule[];
|
|
||||||
label?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const isValid = ref(true);
|
|
||||||
const errorMessage = ref('');
|
|
||||||
|
|
||||||
const onInput = (e: Event) => {
|
|
||||||
const target = e.target as HTMLTextAreaElement;
|
|
||||||
const value = target.value;
|
|
||||||
|
|
||||||
isValid.value = true;
|
|
||||||
errorMessage.value = '';
|
|
||||||
|
|
||||||
props.rules?.forEach(rule => {
|
|
||||||
const result = rule(value);
|
|
||||||
if (result !== true) {
|
|
||||||
isValid.value = false;
|
|
||||||
errorMessage.value = String(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
emit('update:modelValue', value);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.block {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&__inner {
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
color: $secondary;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__textarea {
|
|
||||||
width: 100%;
|
|
||||||
height: 150px;
|
|
||||||
resize: none;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border: 1px solid $border;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
background-color: $main;
|
|
||||||
|
|
||||||
color: $primary_dark;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: $disabled_secondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__error {
|
|
||||||
color: $error;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
animation: fadeInUp 0.3s ease;
|
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
@click="toggleTheme"
|
|
||||||
class="theme"
|
|
||||||
>
|
|
||||||
<icon v-if="theme === 'light'" name="line-md:moon-alt-loop" size="22" />
|
|
||||||
<icon v-else name="line-md:sunny-outline-loop" size="22" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useThemes } from '@composables/themes';
|
|
||||||
|
|
||||||
const { theme, toggleTheme } = useThemes();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.theme {
|
|
||||||
background-color: transparent;
|
|
||||||
border-radius: $less_border_radius;
|
|
||||||
padding: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: $primary;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background-color: $primary_shadow;
|
|
||||||
}
|
|
||||||
|
|
||||||
& span {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="title">
|
|
||||||
<div class="container">
|
|
||||||
<div class="title__wrapper">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.title {
|
|
||||||
padding-block: 50px;
|
|
||||||
background-color: $title_bg;
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.title__wrapper h1) {
|
|
||||||
color: $primary_dark;
|
|
||||||
font-family: "Playfair Display", sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 36px;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
:deep(.title__wrapper p) {
|
|
||||||
max-width: 600px;
|
|
||||||
text-align: center;
|
|
||||||
color: $text;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
:deep(.title__wrapper .search) {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
& span {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
right: 24px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: $disabled_secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
& input {
|
|
||||||
background-color: $main;
|
|
||||||
border: 1px solid $border;
|
|
||||||
border-radius: 50px;
|
|
||||||
padding: 18px 70px 18px 50px;
|
|
||||||
|
|
||||||
color: $text;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: $text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export * from './useLogin';
|
|
||||||
export * from './useLogout';
|
|
||||||
export * from './useNewPassword';
|
|
||||||
export * from './usePasswordReset';
|
|
||||||
export * from './useRefresh';
|
|
||||||
export * from './useRegister';
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
import { useLocaleRedirect } from '@composables/languages';
|
|
||||||
import { useUserBaseData } from '@composables/user';
|
|
||||||
import { LOGIN } from '@graphql/mutations/auth';
|
|
||||||
import type { ILoginResponse } from '@types';
|
|
||||||
|
|
||||||
export function useLogin() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const localePath = useLocalePath();
|
|
||||||
const { $appHelpers, $notify } = useNuxtApp();
|
|
||||||
|
|
||||||
const { checkAndRedirect } = useLocaleRedirect();
|
|
||||||
const { loadUserBaseData } = useUserBaseData();
|
|
||||||
|
|
||||||
const cookieRefresh = useCookie($appHelpers.COOKIES_REFRESH_TOKEN_KEY, {
|
|
||||||
default: () => '',
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
const cookieAccess = useCookie($appHelpers.COOKIES_ACCESS_TOKEN_KEY, {
|
|
||||||
default: () => '',
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
const cookieLocale = useCookie($appHelpers.COOKIES_LOCALE_KEY, {
|
|
||||||
default: () => $appHelpers.DEFAULT_LOCALE,
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate, loading } = useMutation<ILoginResponse>(LOGIN);
|
|
||||||
|
|
||||||
|
|
||||||
async function login(email: string, password: string, isStayLogin: boolean) {
|
|
||||||
try {
|
|
||||||
const result = await mutate({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
const authData = result?.data?.obtainJwtToken;
|
|
||||||
if (!authData) return;
|
|
||||||
|
|
||||||
if (isStayLogin && authData.refreshToken) {
|
|
||||||
cookieRefresh.value = authData.refreshToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
userStore.setUser(authData.user);
|
|
||||||
cookieAccess.value = authData.accessToken;
|
|
||||||
|
|
||||||
navigateTo(localePath('/'));
|
|
||||||
|
|
||||||
$notify({
|
|
||||||
message: t('popup.success.login'),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authData.user.language !== cookieLocale.value) {
|
|
||||||
await checkAndRedirect(authData.user.language);
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadUserBaseData(authData.user.email);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
console.error('useLogin error:', err);
|
|
||||||
let message = t('popup.errors.defaultError');
|
|
||||||
if (isGraphQLError(err)) {
|
|
||||||
message = err.graphQLErrors?.[0]?.message || message;
|
|
||||||
} else {
|
|
||||||
message = err.message;
|
|
||||||
}
|
|
||||||
$notify({
|
|
||||||
message,
|
|
||||||
type: 'error',
|
|
||||||
title: t('popup.errors.main'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
login,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
export function useLogout() {
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const cartStore = useCartStore();
|
|
||||||
const wishlistStore = useWishlistStore();
|
|
||||||
const router = useRouter();
|
|
||||||
const { $appHelpers } = useNuxtApp();
|
|
||||||
|
|
||||||
const cookieRefresh = useCookie($appHelpers.COOKIES_REFRESH_TOKEN_KEY, {
|
|
||||||
default: () => '',
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
const cookieAccess = useCookie($appHelpers.COOKIES_ACCESS_TOKEN_KEY, {
|
|
||||||
default: () => '',
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
async function logout() {
|
|
||||||
userStore.setUser(null);
|
|
||||||
cartStore.setCurrentOrders(null);
|
|
||||||
wishlistStore.setWishlist(null);
|
|
||||||
|
|
||||||
cookieRefresh.value = '';
|
|
||||||
cookieAccess.value = '';
|
|
||||||
|
|
||||||
await router.push({
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
logout,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import { NEW_PASSWORD } from '@graphql/mutations/auth.js';
|
|
||||||
import type { INewPasswordResponse } from '@types';
|
|
||||||
|
|
||||||
export function useNewPassword() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const router = useRouter();
|
|
||||||
const { $notify } = useNuxtApp();
|
|
||||||
const localePath = useLocalePath();
|
|
||||||
|
|
||||||
const token = useRouteQuery('token', '');
|
|
||||||
const uid = useRouteQuery('uid', '');
|
|
||||||
|
|
||||||
const { mutate, loading, error } = useMutation<INewPasswordResponse>(NEW_PASSWORD);
|
|
||||||
|
|
||||||
async function newPassword(password: string, confirmPassword: string) {
|
|
||||||
const result = await mutate({
|
|
||||||
password,
|
|
||||||
confirmPassword,
|
|
||||||
token: token.value,
|
|
||||||
uid: uid.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.data?.confirmResetPassword.success) {
|
|
||||||
$notify({
|
|
||||||
message: t('popup.success.newPassword'),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
await router.push({
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
navigateTo(localePath('/'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(error, (err) => {
|
|
||||||
if (!err) return;
|
|
||||||
console.error('useNewPassword error:', err);
|
|
||||||
let message = t('popup.errors.defaultError');
|
|
||||||
if (isGraphQLError(err)) {
|
|
||||||
message = err.graphQLErrors?.[0]?.message || message;
|
|
||||||
} else {
|
|
||||||
message = err.message;
|
|
||||||
}
|
|
||||||
$notify({
|
|
||||||
message,
|
|
||||||
type: 'error',
|
|
||||||
title: t('popup.errors.main'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
newPassword,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { RESET_PASSWORD } from '@graphql/mutations/auth.js';
|
|
||||||
import type { IPasswordResetResponse } from '@types';
|
|
||||||
|
|
||||||
export function usePasswordReset() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const { $notify } = useNuxtApp();
|
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
const { mutate, loading, error } = useMutation<IPasswordResetResponse>(RESET_PASSWORD);
|
|
||||||
|
|
||||||
async function resetPassword(email: string) {
|
|
||||||
const result = await mutate({
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.data?.resetPassword.success) {
|
|
||||||
$notify({
|
|
||||||
message: t('popup.success.reset'),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
appStore.unsetActiveAuthState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(error, (err) => {
|
|
||||||
if (!err) return;
|
|
||||||
console.error('usePasswordReset error:', err);
|
|
||||||
let message = t('popup.errors.defaultError');
|
|
||||||
if (isGraphQLError(err)) {
|
|
||||||
message = err.graphQLErrors?.[0]?.message || message;
|
|
||||||
} else {
|
|
||||||
message = err.message;
|
|
||||||
}
|
|
||||||
$notify({
|
|
||||||
message,
|
|
||||||
type: 'error',
|
|
||||||
title: t('popup.errors.main'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
resetPassword,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
import { useLogout } from '@composables/auth';
|
|
||||||
import { useLocaleRedirect } from '@composables/languages';
|
|
||||||
import { useUserBaseData } from '@composables/user';
|
|
||||||
import { REFRESH } from '@graphql/mutations/auth';
|
|
||||||
|
|
||||||
export function useRefresh() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const router = useRouter();
|
|
||||||
const localePath = useLocalePath();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const { $appHelpers, $notify } = useNuxtApp();
|
|
||||||
|
|
||||||
const { checkAndRedirect } = useLocaleRedirect();
|
|
||||||
const { loadUserBaseData } = useUserBaseData();
|
|
||||||
const { logout } = useLogout();
|
|
||||||
|
|
||||||
const { mutate, loading, error } = useMutation(REFRESH);
|
|
||||||
|
|
||||||
function isTokenInvalidError(error: unknown): boolean {
|
|
||||||
if (isGraphQLError(error)) {
|
|
||||||
const message = error.graphQLErrors?.[0]?.message?.toLowerCase() || '';
|
|
||||||
return (
|
|
||||||
message.includes('invalid refresh token') ||
|
|
||||||
message.includes('blacklist') ||
|
|
||||||
message.includes('expired') ||
|
|
||||||
message.includes('revoked')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
const cookieRefresh = useCookie($appHelpers.COOKIES_REFRESH_TOKEN_KEY, {
|
|
||||||
default: () => '',
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
const cookieAccess = useCookie($appHelpers.COOKIES_ACCESS_TOKEN_KEY, {
|
|
||||||
default: () => '',
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
const cookieLocale = useCookie($appHelpers.COOKIES_LOCALE_KEY, {
|
|
||||||
default: () => $appHelpers.DEFAULT_LOCALE,
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!cookieRefresh.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await mutate({
|
|
||||||
refreshToken: cookieRefresh.value,
|
|
||||||
});
|
|
||||||
const data = result?.data?.refreshJwtToken;
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
userStore.setUser(data.user);
|
|
||||||
cookieRefresh.value = data.refreshToken;
|
|
||||||
cookieAccess.value = data.accessToken;
|
|
||||||
|
|
||||||
if (data.user.language !== cookieLocale.value) {
|
|
||||||
await checkAndRedirect(data.user.language);
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadUserBaseData(data.user.email);
|
|
||||||
} catch (err) {
|
|
||||||
if (isTokenInvalidError(err)) {
|
|
||||||
await logout();
|
|
||||||
await router.push(localePath('/'));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = t('popup.errors.defaultError');
|
|
||||||
if (isGraphQLError(err)) {
|
|
||||||
message = err.graphQLErrors?.[0]?.message || message;
|
|
||||||
} else if (err instanceof Error) {
|
|
||||||
message = err.message;
|
|
||||||
} else if (typeof err === 'string') {
|
|
||||||
message = err;
|
|
||||||
}
|
|
||||||
|
|
||||||
$notify({
|
|
||||||
message,
|
|
||||||
type: 'error',
|
|
||||||
title: t('popup.errors.main'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(error, async (err) => {
|
|
||||||
if (!err) return;
|
|
||||||
|
|
||||||
if (isTokenInvalidError(err)) {
|
|
||||||
await logout();
|
|
||||||
await router.push(localePath('/'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('useRefresh error:', err);
|
|
||||||
let message = t('popup.errors.defaultError');
|
|
||||||
if (isGraphQLError(err)) {
|
|
||||||
message = err.graphQLErrors?.[0]?.message || message;
|
|
||||||
} else {
|
|
||||||
message = err.message;
|
|
||||||
}
|
|
||||||
$notify({
|
|
||||||
message,
|
|
||||||
type: 'error',
|
|
||||||
title: t('popup.errors.main'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
refresh,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import { useMailClient } from '@composables/utils';
|
|
||||||
import { REGISTER } from '@graphql/mutations/auth.js';
|
|
||||||
import type { IRegisterResponse } from '@types';
|
|
||||||
|
|
||||||
interface IRegisterArguments {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
phoneNumber: string;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
confirmPassword: string;
|
|
||||||
referrer: string;
|
|
||||||
isSubscribed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRegister() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const { $notify } = useNuxtApp();
|
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
const { mailClientUrl, detectMailClient, openMailClient } = useMailClient();
|
|
||||||
|
|
||||||
const { mutate, loading, error } = useMutation<IRegisterResponse>(REGISTER);
|
|
||||||
|
|
||||||
async function register(payload: IRegisterArguments) {
|
|
||||||
const result = await mutate({
|
|
||||||
firstName: payload.firstName,
|
|
||||||
lastName: payload.lastName,
|
|
||||||
phoneNumber: payload.phoneNumber,
|
|
||||||
email: payload.email,
|
|
||||||
password: payload.password,
|
|
||||||
confirmPassword: payload.confirmPassword,
|
|
||||||
referrer: payload.referrer,
|
|
||||||
isSubscribed: payload.isSubscribed,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.data?.createUser?.success) {
|
|
||||||
detectMailClient(payload.email);
|
|
||||||
|
|
||||||
$notify({
|
|
||||||
message: h('div', [
|
|
||||||
h('p', t('popup.success.register')),
|
|
||||||
mailClientUrl.value
|
|
||||||
? h(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
class: 'el-notification__button',
|
|
||||||
onClick: () => {
|
|
||||||
openMailClient();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
t('buttons.goEmail'),
|
|
||||||
)
|
|
||||||
: '',
|
|
||||||
]),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
appStore.unsetActiveAuthState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(error, (err) => {
|
|
||||||
if (!err) return;
|
|
||||||
console.error('useRegister error:', err);
|
|
||||||
let message = t('popup.errors.defaultError');
|
|
||||||
if (isGraphQLError(err)) {
|
|
||||||
message = err.graphQLErrors?.[0]?.message || message;
|
|
||||||
} else {
|
|
||||||
message = err.message;
|
|
||||||
}
|
|
||||||
$notify({
|
|
||||||
message,
|
|
||||||
type: 'error',
|
|
||||||
title: t('popup.errors.main'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
register,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './useBrandBySlug';
|
|
||||||
export * from './useBrands';
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { GET_BRAND_BY_SLUG } from '@graphql/queries/standalone/brands';
|
|
||||||
import type { IBrandsResponse } from '@types';
|
|
||||||
|
|
||||||
export async function useBrandBySlug(slug: string) {
|
|
||||||
const brand = computed(() => data.value?.brands.edges[0]?.node ?? null);
|
|
||||||
|
|
||||||
const { data, error } = await useAsyncQuery<IBrandsResponse>(GET_BRAND_BY_SLUG, {
|
|
||||||
slug,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!data.value?.brands?.edges?.length) {
|
|
||||||
throw createError({
|
|
||||||
status: 404,
|
|
||||||
statusText: 'Brand not found',
|
|
||||||
fatal: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(error, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('useBrandsBySlug error:', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
brand,
|
|
||||||
seoMeta: computed(() => brand.value?.seoMeta),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import { GET_BRANDS } from '@graphql/queries/standalone/brands';
|
|
||||||
import type { IBrand, IBrandsResponse } from '@types';
|
|
||||||
|
|
||||||
interface IBrandArgs {
|
|
||||||
brandAfter?: string;
|
|
||||||
brandOrderBy?: string;
|
|
||||||
brandName?: string;
|
|
||||||
brandSearch?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IBrandVars {
|
|
||||||
brandFirst: number;
|
|
||||||
brandAfter?: string;
|
|
||||||
brandOrderBy?: string;
|
|
||||||
brandName?: string;
|
|
||||||
brandSearch?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useBrands(args: IBrandArgs = {}) {
|
|
||||||
const variables = reactive<IBrandVars>({
|
|
||||||
brandFirst: 45,
|
|
||||||
brandAfter: args.brandAfter,
|
|
||||||
brandOrderBy: args.orderBy,
|
|
||||||
brandName: args.brandName,
|
|
||||||
brandSearch: args.brandSearch,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pending = ref<boolean>(false);
|
|
||||||
const brands = ref<IBrand[]>([]);
|
|
||||||
const pageInfo = ref<{
|
|
||||||
hasNextPage: boolean;
|
|
||||||
endCursor: string;
|
|
||||||
}>({
|
|
||||||
hasNextPage: false,
|
|
||||||
endCursor: '',
|
|
||||||
});
|
|
||||||
const error = ref<string | null>(null);
|
|
||||||
|
|
||||||
const getBrands = async (): Promise<void> => {
|
|
||||||
pending.value = true;
|
|
||||||
|
|
||||||
const queryVariables = {
|
|
||||||
brandFirst: variables.first,
|
|
||||||
brandAfter: variables.brandAfter || undefined,
|
|
||||||
brandOrderBy: variables.orderBy || undefined,
|
|
||||||
brandName: variables.brandName || undefined,
|
|
||||||
brandSearch: variables.brandSearch || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data, error: mistake } = await useAsyncQuery<IBrandsResponse>(GET_BRANDS, queryVariables);
|
|
||||||
|
|
||||||
if (data.value?.brands?.edges) {
|
|
||||||
pageInfo.value = data.value?.brands.pageInfo;
|
|
||||||
|
|
||||||
if (variables.brandAfter) {
|
|
||||||
brands.value = [
|
|
||||||
...brands.value,
|
|
||||||
...data.value.brands.edges,
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
brands.value = data.value?.brands.edges;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mistake.value) {
|
|
||||||
error.value = mistake.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
pending.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(error, (e) => {
|
|
||||||
if (e) console.error('useBrands error:', e);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
pending,
|
|
||||||
brands,
|
|
||||||
pageInfo,
|
|
||||||
variables,
|
|
||||||
getBrands,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from './useBreadcrumbs';
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import type { ICategory, IProduct } from '@types';
|
|
||||||
|
|
||||||
interface Crumb {
|
|
||||||
text: string;
|
|
||||||
link?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findCategoryPath(
|
|
||||||
nodes: ICategory[],
|
|
||||||
targetSlug: string,
|
|
||||||
path: ICategory[] = [],
|
|
||||||
): ICategory[] | null {
|
|
||||||
for (const node of nodes) {
|
|
||||||
const newPath = [
|
|
||||||
...path,
|
|
||||||
node,
|
|
||||||
];
|
|
||||||
if (node.slug === targetSlug) {
|
|
||||||
return newPath;
|
|
||||||
}
|
|
||||||
if (node.children?.length) {
|
|
||||||
const found = findCategoryPath(node.children, targetSlug, newPath);
|
|
||||||
if (found) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useBreadcrumbs() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const route = useRoute();
|
|
||||||
const pageTitle = useState<string>('pageTitle');
|
|
||||||
const categoryStore = useCategoryStore();
|
|
||||||
const product = useState<IProduct | null>('currentProduct');
|
|
||||||
|
|
||||||
const breadcrumbs = computed<Crumb[]>(() => {
|
|
||||||
const crumbs: Crumb[] = [
|
|
||||||
{
|
|
||||||
text: t('breadcrumbs.home'),
|
|
||||||
link: '/',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (route.path.includes('/catalog') || route.path.includes('/product')) {
|
|
||||||
crumbs.push({
|
|
||||||
text: t('breadcrumbs.catalog'),
|
|
||||||
link: '/catalog',
|
|
||||||
});
|
|
||||||
|
|
||||||
let categorySlug: string | undefined;
|
|
||||||
if (route.path.includes('/catalog')) {
|
|
||||||
categorySlug = route.params.categorySlug as string;
|
|
||||||
} else if (route.path.includes('/product')) {
|
|
||||||
categorySlug = product.value?.category?.slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (categorySlug) {
|
|
||||||
const roots = categoryStore.categories.map((e) => e.node);
|
|
||||||
const path = findCategoryPath(roots, categorySlug);
|
|
||||||
path?.forEach((node) => {
|
|
||||||
crumbs.push({
|
|
||||||
text: node.name,
|
|
||||||
link: `/catalog/${node.slug}`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.path.includes('/product') && product.value) {
|
|
||||||
crumbs.push({
|
|
||||||
text: product.value.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const routeNameWithoutLocale = String(route.name).split('___')[0];
|
|
||||||
crumbs.push({
|
|
||||||
text: pageTitle.value || t(`breadcrumbs.${routeNameWithoutLocale}`),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return crumbs;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
breadcrumbs,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './useCategories';
|
|
||||||
export * from './useCategoryBySlug';
|
|
||||||
export * from './useCategoryBySlugSeo';
|
|
||||||
export * from './useCategoryTags';
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { GET_CATEGORIES } from '@graphql/queries/standalone/categories';
|
|
||||||
import type { ICategoriesResponse } from '@types';
|
|
||||||
|
|
||||||
export async function useCategories() {
|
|
||||||
const categoryStore = useCategoryStore();
|
|
||||||
const { locale } = useI18n();
|
|
||||||
|
|
||||||
const getCategories = async (cursor?: string): Promise<void> => {
|
|
||||||
const { data, error } = await useAsyncQuery<ICategoriesResponse>(GET_CATEGORIES, {
|
|
||||||
level: 0,
|
|
||||||
whole: true,
|
|
||||||
categoryAfter: cursor,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!error.value && data.value?.categories.edges) {
|
|
||||||
if (!cursor) {
|
|
||||||
categoryStore.setCategories(data.value.categories.edges);
|
|
||||||
} else {
|
|
||||||
categoryStore.addCategories(data.value.categories.edges);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageInfo = data.value.categories.pageInfo;
|
|
||||||
if (pageInfo?.hasNextPage && pageInfo.endCursor) {
|
|
||||||
await getCategories(pageInfo.endCursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.value) console.error('useCategories error:', error.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(locale, async () => {
|
|
||||||
categoryStore.setCategories([]);
|
|
||||||
await getCategories();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
getCategories,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue