Features: 1) Introduce new components including ui-counter, ui-link, base-auth, and base-header-catalogue with scoped styles; 2) Add useProductTags composable and integrate GraphQL queries for product tagging; 3) Build standalone pages for cart and wishlist with basic templates; 4) Integrate vue3-marquee-slider, swiper, and primeicons dependencies for enhanced UI interactions; 5) Add skeleton loaders for language switcher and counter components; 6) Localize the app with support for it-it, de-de, ja-jp, da-dk, fr-fr, and nl-nl locales;
Fixes: 1) Refactor `useProducts` and `useCategorybySlug` composables for improved error handling and lazy loading; 2) Correct import path in `product-page.vue` for `useProductBySlug`; 3) Update `useLanguages` composable to set current locale from local storage; 4) Remove unused `auth.js`, `base-header.vue`, and deprecated GraphQL fragments; Extra: Minor styling adjustments and removal of redundant console logs; Updated `package-lock.json` dependencies for version consistency.
42
storefront/package-lock.json
generated
|
|
@ -15,9 +15,12 @@
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"pinia": "^3.0.1",
|
"pinia": "^3.0.1",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
|
"swiper": "^11.2.8",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-i18n": "^11.1.4",
|
"vue-i18n": "^11.1.4",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0",
|
||||||
|
"vue3-marquee-slider": "^1.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
|
|
@ -3344,6 +3347,12 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/primeicons": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
|
@ -3618,6 +3627,25 @@
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swiper": {
|
||||||
|
"version": "11.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.8.tgz",
|
||||||
|
"integrity": "sha512-S5FVf6zWynPWooi7pJ7lZhSUe2snTzqLuUzbd5h5PHUOhzgvW0bLKBd2wv0ixn6/5o9vwc/IkQT74CRcLJQzeg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/swiperjs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "open_collective",
|
||||||
|
"url": "http://opencollective.com/swiper"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/symbol-observable": {
|
"node_modules/symbol-observable": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||||
|
|
@ -4050,6 +4078,18 @@
|
||||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/vue3-marquee-slider": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue3-marquee-slider/-/vue3-marquee-slider-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-92nrvGrmTC9Ltqz6QRXdkuJ5Tj6gM+69xy8sEB2kq/xY8RI/FTQxDF6QiCyJdieJDNL67giKlhJVDL0D9i9SxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.2.45"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,12 @@
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"pinia": "^3.0.1",
|
"pinia": "^3.0.1",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
|
"swiper": "^11.2.8",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-i18n": "^11.1.4",
|
"vue-i18n": "^11.1.4",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0",
|
||||||
|
"vue3-marquee-slider": "^1.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 41 KiB |
|
|
@ -1,11 +1,21 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
import {onMounted} from "vue";
|
import {computed, onMounted} from "vue";
|
||||||
import {useRefresh} from "@/composables/auth";
|
import {useRefresh} from "@/composables/auth";
|
||||||
import {useCompanyInfo} from "@/composables/company";
|
import {useCompanyInfo} from "@/composables/company";
|
||||||
import {useLanguages} from "@/composables/languages/index.js";
|
import {useLanguages} from "@/composables/languages/index.js";
|
||||||
import BaseHeader from "@/components/base/base-header.vue";
|
import BaseHeader from "@/components/base/header/base-header.vue";
|
||||||
import BaseFooter from "@/components/base/base-footer.vue";
|
import BaseFooter from "@/components/base/base-footer.vue";
|
||||||
|
import BaseAuth from "@/components/base/base-auth.vue";
|
||||||
|
import LoginForm from "@/components/forms/login-form.vue";
|
||||||
|
import RegisterForm from "@/components/forms/register-form.vue";
|
||||||
|
import NewPasswordForm from "@/components/forms/new-password-form.vue";
|
||||||
|
import ResetPasswordForm from "@/components/forms/reset-password-form.vue";
|
||||||
|
import {useAppStore} from "@/stores/app.js";
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const activeState = computed(() => appStore.activeState)
|
||||||
|
|
||||||
const { refresh } = useRefresh();
|
const { refresh } = useRefresh();
|
||||||
const { getCompanyInfo } = useCompanyInfo();
|
const { getCompanyInfo } = useCompanyInfo();
|
||||||
|
|
@ -25,6 +35,14 @@ onMounted(async () => {
|
||||||
<template>
|
<template>
|
||||||
<main class="main" id="top">
|
<main class="main" id="top">
|
||||||
<base-header />
|
<base-header />
|
||||||
|
<Transition name="opacity" mode="out-in">
|
||||||
|
<base-auth v-if="activeState">
|
||||||
|
<login-form v-if="activeState === 'login'" />
|
||||||
|
<register-form v-if="activeState === 'register'" />
|
||||||
|
<reset-password-form v-if="activeState === 'reset-password'" />
|
||||||
|
<new-password-form v-if="activeState === 'new-password'" />
|
||||||
|
</base-auth>
|
||||||
|
</Transition>
|
||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
<Transition name="opacity" mode="out-in">
|
<Transition name="opacity" mode="out-in">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
|
|
@ -35,5 +53,13 @@ onMounted(async () => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.main {
|
||||||
|
padding-top: 90px;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-skeleton__item) {
|
||||||
|
--el-skeleton-color: #d0d2d3 !important;
|
||||||
|
--el-skeleton-to-color: #b4b4b7 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client/core'
|
import {ApolloClient, ApolloLink, createHttpLink, InMemoryCache} from '@apollo/client/core'
|
||||||
import { setContext } from '@apollo/client/link/context'
|
import { setContext } from '@apollo/client/link/context'
|
||||||
import {DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY} from "@/config/index.js";
|
import {DEFAULT_LOCALE, LOCALE_STORAGE_ACCESS_TOKEN_KEY, LOCALE_STORAGE_LOCALE_KEY} from "@/config/index.js";
|
||||||
import {computed} from "vue";
|
import {computed} from "vue";
|
||||||
import { useAuthStore } from "@/stores/auth.js";
|
|
||||||
|
|
||||||
const httpLink = createHttpLink({
|
const httpLink = createHttpLink({
|
||||||
uri: 'https://api.' + import.meta.env.EVIBES_BASE_DOMAIN + '/graphql/',
|
uri: 'https://api.' + import.meta.env.EVIBES_BASE_DOMAIN + '/graphql/',
|
||||||
|
|
@ -13,10 +12,8 @@ const userLocale = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createApolloClient = () => {
|
export const createApolloClient = () => {
|
||||||
const authStore = useAuthStore()
|
|
||||||
|
|
||||||
const accessToken = computed(() => {
|
const accessToken = computed(() => {
|
||||||
return authStore.accessToken
|
return localStorage.getItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY)
|
||||||
})
|
})
|
||||||
|
|
||||||
const authLink = setContext((_, { headers }) => {
|
const authLink = setContext((_, { headers }) => {
|
||||||
|
|
|
||||||
BIN
storefront/src/assets/fonts/SourceCodePro/SourceCodePro-Bold.ttf
Normal file
|
|
@ -1,5 +0,0 @@
|
||||||
<svg width="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
||||||
d="M19.7071 5.70711C20.0976 5.31658 20.0976 4.68342 19.7071 4.29289C19.3166 3.90237 18.6834 3.90237 18.2929 4.29289L14.032 8.55382C13.4365 8.20193 12.7418 8 12 8C9.79086 8 8 9.79086 8 12C8 12.7418 8.20193 13.4365 8.55382 14.032L4.29289 18.2929C3.90237 18.6834 3.90237 19.3166 4.29289 19.7071C4.68342 20.0976 5.31658 20.0976 5.70711 19.7071L9.96803 15.4462C10.5635 15.7981 11.2582 16 12 16C14.2091 16 16 14.2091 16 12C16 11.2582 15.7981 10.5635 15.4462 9.96803L19.7071 5.70711ZM12.518 10.0677C12.3528 10.0236 12.1792 10 12 10C10.8954 10 10 10.8954 10 12C10 12.1792 10.0236 12.3528 10.0677 12.518L12.518 10.0677ZM11.482 13.9323L13.9323 11.482C13.9764 11.6472 14 11.8208 14 12C14 13.1046 13.1046 14 12 14C11.8208 14 11.6472 13.9764 11.482 13.9323ZM15.7651 4.8207C14.6287 4.32049 13.3675 4 12 4C9.14754 4 6.75717 5.39462 4.99812 6.90595C3.23268 8.42276 2.00757 10.1376 1.46387 10.9698C1.05306 11.5985 1.05306 12.4015 1.46387 13.0302C1.92276 13.7326 2.86706 15.0637 4.21194 16.3739L5.62626 14.9596C4.4555 13.8229 3.61144 12.6531 3.18002 12C3.6904 11.2274 4.77832 9.73158 6.30147 8.42294C7.87402 7.07185 9.81574 6 12 6C12.7719 6 13.5135 6.13385 14.2193 6.36658L15.7651 4.8207ZM12 18C11.2282 18 10.4866 17.8661 9.78083 17.6334L8.23496 19.1793C9.37136 19.6795 10.6326 20 12 20C14.8525 20 17.2429 18.6054 19.002 17.0941C20.7674 15.5772 21.9925 13.8624 22.5362 13.0302C22.947 12.4015 22.947 11.5985 22.5362 10.9698C22.0773 10.2674 21.133 8.93627 19.7881 7.62611L18.3738 9.04043C19.5446 10.1771 20.3887 11.3469 20.8201 12C20.3097 12.7726 19.2218 14.2684 17.6986 15.5771C16.1261 16.9282 14.1843 18 12 18Z"
|
|
||||||
fill="#000000"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,3 +0,0 @@
|
||||||
<svg width="24" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" fill="none">
|
|
||||||
<path fill="#000000" fill-rule="evenodd" d="M3.415 10.242c-.067-.086-.13-.167-.186-.242a16.806 16.806 0 011.803-2.025C6.429 6.648 8.187 5.5 10 5.5c1.813 0 3.57 1.148 4.968 2.475A16.816 16.816 0 0116.771 10a16.9 16.9 0 01-1.803 2.025C13.57 13.352 11.813 14.5 10 14.5c-1.813 0-3.57-1.148-4.968-2.475a16.799 16.799 0 01-1.617-1.783zm15.423-.788L18 10l.838.546-.002.003-.003.004-.01.016-.037.054a17.123 17.123 0 01-.628.854 18.805 18.805 0 01-1.812 1.998C14.848 14.898 12.606 16.5 10 16.5s-4.848-1.602-6.346-3.025a18.806 18.806 0 01-2.44-2.852 6.01 6.01 0 01-.037-.054l-.01-.016-.003-.004-.001-.002c0-.001-.001-.001.837-.547l-.838-.546.002-.003.003-.004.01-.016a6.84 6.84 0 01.17-.245 18.804 18.804 0 012.308-2.66C5.151 5.1 7.394 3.499 10 3.499s4.848 1.602 6.346 3.025a18.803 18.803 0 012.44 2.852l.037.054.01.016.003.004.001.002zM18 10l.838-.546.355.546-.355.546L18 10zM1.162 9.454L2 10l-.838.546L.807 10l.355-.546zM9 10a1 1 0 112 0 1 1 0 01-2 0zm1-3a3 3 0 100 6 3 3 0 000-6z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1 KiB |
BIN
storefront/src/assets/images/evibes-big-simple.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
storefront/src/assets/images/evibes-big.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
storefront/src/assets/images/homeBg.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* ===== 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;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
$font_default: '', sans-serif;
|
$font_default: 'Source Code Pro', sans-serif;
|
||||||
|
|
||||||
$white: #ffffff;
|
$white: #ffffff;
|
||||||
$black: #000000;
|
$black: #000000;
|
||||||
|
$accent: #7965d1;
|
||||||
|
$accentLight: #a69cdc;
|
||||||
|
$accentDisabled: #826fa2;
|
||||||
$error: #f13838;
|
$error: #f13838;
|
||||||
|
$default_border_radius: 4px;
|
||||||
65
storefront/src/components/base/base-auth.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<div class="auth">
|
||||||
|
<div class="auth__content" ref="modalRef">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useAppStore} from "@/stores/app.js";
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {onClickOutside} from "@vueuse/core";
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
appStore.setActiveState(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
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: $white;
|
||||||
|
width: 600px;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.auth {
|
||||||
|
&__content {
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.auth {
|
||||||
|
&__content {
|
||||||
|
padding: 20px 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<template>
|
|
||||||
<header class="header">
|
|
||||||
<div class="container">
|
|
||||||
<div class="header__wrapper">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {onMounted} from "vue";
|
|
||||||
import {useCategories} from "@/composables/categories";
|
|
||||||
|
|
||||||
const { categories, loading, getCategories } = useCategories();
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await getCategories()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.header {
|
|
||||||
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<div class="catalogue">
|
||||||
|
<button class="catalogue__button">
|
||||||
|
{{ t('header.catalogue.title') }}
|
||||||
|
<span>▽</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
|
const {t} = useI18n()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.catalogue {
|
||||||
|
&__button {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
background-color: rgba($accent, 0.2);
|
||||||
|
border: 1px solid $accent;
|
||||||
|
padding: 5px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
color: $accent;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background-color: $accent;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
& span {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
237
storefront/src/components/base/header/base-header-search.vue
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
<template>
|
||||||
|
<div class="search">
|
||||||
|
<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')"
|
||||||
|
/>
|
||||||
|
<div class="search__tools">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="clearSearch"
|
||||||
|
v-if="query"
|
||||||
|
>
|
||||||
|
<i class="pi pi-times"></i>
|
||||||
|
</button>
|
||||||
|
<div class="search__tools-line" v-if="query"></div>
|
||||||
|
<button type="submit">
|
||||||
|
<i class="pi pi-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="search__results" :class="[{ active: (searchResults && isSearchActive) || loading }]">
|
||||||
|
<header-search-skeleton v-if="loading" />
|
||||||
|
<div
|
||||||
|
class="search__results-inner"
|
||||||
|
v-for="(blocks, item) in filteredSearchResults"
|
||||||
|
:key="item"
|
||||||
|
>
|
||||||
|
<div class="search__results-title">
|
||||||
|
<p>{{ getBlockTitle(item) }}:</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="search__item"
|
||||||
|
v-for="item in blocks"
|
||||||
|
:key="item.uuid"
|
||||||
|
>
|
||||||
|
<div class="search__item-left">
|
||||||
|
<i class="pi pi-search"></i>
|
||||||
|
<p>{{ item.name }}</p>
|
||||||
|
</div>
|
||||||
|
<i class="pi pi-external-link"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="search__results-empty" v-if="!hasResults && query && !loading">
|
||||||
|
<p>{{ t('header.search.empty') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="search__bg"
|
||||||
|
@click="toggleSearch(false)"
|
||||||
|
v-if="isSearchActive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import HeaderSearchSkeleton from "@/components/skeletons/header/header-search-skeleton.vue";
|
||||||
|
import { useSearchUI } from "@/composables/search";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
query,
|
||||||
|
isSearchActive,
|
||||||
|
loading,
|
||||||
|
searchResults,
|
||||||
|
filteredSearchResults,
|
||||||
|
hasResults,
|
||||||
|
getBlockTitle,
|
||||||
|
clearSearch,
|
||||||
|
toggleSearch
|
||||||
|
} = useSearchUI();
|
||||||
|
|
||||||
|
function submitSearch() {
|
||||||
|
if (query.value) {
|
||||||
|
router.push({
|
||||||
|
name: 'search',
|
||||||
|
query: { q: query.value }
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleSearch(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
height: 45px;
|
||||||
|
|
||||||
|
&__bg {
|
||||||
|
background-color: #0000001a;
|
||||||
|
height: 100vh;
|
||||||
|
left: 0;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
transition: 0.2s;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $white;
|
||||||
|
box-shadow: 0 0 0 1px #0000000a,0 4px 4px #0000000a,0 20px 40px #00000014;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background-color: $white;
|
||||||
|
box-shadow: 0 0 0 1px #0000000a,0 4px 4px #0000000a,0 20px 40px #00000014;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__form {
|
||||||
|
width: 100%;
|
||||||
|
height: 45px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& input {
|
||||||
|
background-color: transparent;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-inline: 20px 150px;
|
||||||
|
border: 1px solid #dedede;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
|
||||||
|
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;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border: 1px solid $accent;
|
||||||
|
background-color: rgba($accent, 0.2);
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
font-size: 12px;
|
||||||
|
color: $accent;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background-color: rgba($accent, 1);
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-line {
|
||||||
|
background-color: $accent;
|
||||||
|
height: 15px;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__results {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
max-height: 40vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
background-color: rgba($accent, 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;
|
||||||
|
transition: 0.2s;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background-color: #efefef;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
& i {
|
||||||
|
color: #7c7c7c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
129
storefront/src/components/base/header/base-header.vue
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
<template>
|
||||||
|
<header class="header">
|
||||||
|
<router-link :to="translations.i18nRoute({ name: 'home' })">
|
||||||
|
<img class="header__logo" src="@images/evibes-big-simple.png" alt="logo">
|
||||||
|
</router-link>
|
||||||
|
<base-header-catalogue />
|
||||||
|
<base-header-search />
|
||||||
|
<div class="header__actions">
|
||||||
|
<router-link :to="translations.i18nRoute({ name: 'wishlist' })" class="header__actions-item">
|
||||||
|
<div>
|
||||||
|
<ui-counter>0</ui-counter>
|
||||||
|
<!-- <counter-skeleton />-->
|
||||||
|
<i class="pi pi-heart"></i>
|
||||||
|
</div>
|
||||||
|
<p>{{ t('header.actions.wishlist') }}</p>
|
||||||
|
</router-link>
|
||||||
|
<router-link :to="translations.i18nRoute({ name: 'cart' })" class="header__actions-item">
|
||||||
|
<div>
|
||||||
|
<ui-counter>0</ui-counter>
|
||||||
|
<!-- <counter-skeleton />-->
|
||||||
|
<i class="pi pi-shopping-cart"></i>
|
||||||
|
</div>
|
||||||
|
<p>{{ t('header.actions.cart') }}</p>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
:to="translations.i18nRoute({ name: 'home' })"
|
||||||
|
class="header__actions-item"
|
||||||
|
v-if="isAuthenticated"
|
||||||
|
>
|
||||||
|
<i class="pi pi-user"></i>
|
||||||
|
<p>{{ t('header.actions.user') }}</p>
|
||||||
|
</router-link>
|
||||||
|
<div
|
||||||
|
class="header__actions-item"
|
||||||
|
@click="appStore.setActiveState('login')"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<i class="pi pi-user"></i>
|
||||||
|
<p>{{ t('header.actions.user') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ui-language-switcher />
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {computed, onMounted} from "vue";
|
||||||
|
import {useCategories} from "@/composables/categories/index.js";
|
||||||
|
import translations from "@/core/helpers/translations.js";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import BaseHeaderSearch from "@/components/base/header/base-header-search.vue";
|
||||||
|
import UiLanguageSwitcher from "@/components/ui/ui-language-switcher.vue";
|
||||||
|
import {useUserStore} from "@/stores/user.js";
|
||||||
|
import {useAppStore} from "@/stores/app.js";
|
||||||
|
import UiCounter from "@/components/ui/ui-counter.vue";
|
||||||
|
import CounterSkeleton from "@/components/skeletons/ui/counter-skeleton.vue";
|
||||||
|
import BaseHeaderCatalogue from "@/components/base/header/base-header-catalogue.vue";
|
||||||
|
|
||||||
|
//TODO: add categories to header
|
||||||
|
|
||||||
|
const {t} = useI18n()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const isAuthenticated = computed(() => userStore.user)
|
||||||
|
|
||||||
|
const { categories, loading, getCategories } = useCategories();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getCategories()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header {
|
||||||
|
box-shadow: 0 1px 2px #0000001a;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
background-color: $white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 50px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
|
||||||
|
&__logo {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
& div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
& i {
|
||||||
|
transition: 0.2s;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& p {
|
||||||
|
transition: 0.2s;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
127
storefront/src/components/cards/product-card.vue
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<router-link
|
||||||
|
:to="translations.i18nRoute({ name: 'product', params: { productSlug: product.slug } })"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="card__image"
|
||||||
|
:src="product.images.edges[0].node.image"
|
||||||
|
:alt="product.name"
|
||||||
|
>
|
||||||
|
</router-link>
|
||||||
|
<div class="card__content">
|
||||||
|
<p class="card__price">{{ product.price }}</p>
|
||||||
|
<p class="card__name">{{ product.name }}</p>
|
||||||
|
<el-rate
|
||||||
|
v-model="rating"
|
||||||
|
size="large"
|
||||||
|
allow-half
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<div class="card__quantity">{{ t('cards.product.stock') }} {{ product.quantity }}</div>
|
||||||
|
<div class="card__bottom">
|
||||||
|
<ui-button class="card__bottom-button">
|
||||||
|
{{ t('buttons.addToCart') }}
|
||||||
|
</ui-button>
|
||||||
|
<div class="card__bottom-wishlist">
|
||||||
|
<i class="pi pi-heart"></i>
|
||||||
|
<!-- <i class="pi pi-heart-fill"></i>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import UiButton from "@/components/ui/ui-button.vue";
|
||||||
|
import translations from "@/core/helpers/translations.js";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
product: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
const {t} = useI18n()
|
||||||
|
|
||||||
|
const rating = computed(() => {
|
||||||
|
return props.product.feedbacks.edges[0] ? props.product.feedbacks.edges[0]?.node?.rating : 5
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card {
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
border: 1px solid $black;
|
||||||
|
width: 340px;
|
||||||
|
background-color: $white;
|
||||||
|
transition: 0.2s;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__image {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__price {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
word-break: break-all;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__quantity {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bottom {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 5px;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
width: 84%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-wishlist {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: $accent;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
font-size: 22px;
|
||||||
|
color: $white;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background-color: $accentLight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -30,23 +30,23 @@ import {computed, ref} from "vue";
|
||||||
import UiButton from "@/components/ui/ui-button.vue";
|
import UiButton from "@/components/ui/ui-button.vue";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useDeposit} from "@/composables/user/useDeposit.js";
|
import {useDeposit} from "@/composables/user/useDeposit.js";
|
||||||
|
import {useCompanyStore} from "@/stores/company.js";
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
|
const companyStore = useCompanyStore()
|
||||||
|
|
||||||
|
const paymentMin = computed(() => companyStore.companyInfo?.paymentGatewayMinimum)
|
||||||
|
const paymentMax = computed(() => companyStore.companyInfo?.paymentGatewayMaximum)
|
||||||
|
|
||||||
const amount = ref('')
|
const amount = ref('')
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return (
|
return (
|
||||||
amount.value >= 5 && amount.value <= 500
|
amount.value >= paymentMin.value &&
|
||||||
|
amount.value <= paymentMax.value
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const onlyNumbersKeypress = (event) => {
|
|
||||||
if (!/\d/.test(event.key)) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { deposit, loading } = useDeposit();
|
const { deposit, loading } = useDeposit();
|
||||||
|
|
||||||
async function handleDeposit() {
|
async function handleDeposit() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="handleLogin()" class="form">
|
<form @submit.prevent="handleLogin()" class="form">
|
||||||
|
<h2 class="form__title">{{ t('forms.login.title') }}</h2>
|
||||||
<ui-input
|
<ui-input
|
||||||
:type="'email'"
|
:type="'email'"
|
||||||
:placeholder="t('fields.email')"
|
:placeholder="t('fields.email')"
|
||||||
|
|
@ -17,13 +18,26 @@
|
||||||
>
|
>
|
||||||
{{ t('checkboxes.remember') }}
|
{{ t('checkboxes.remember') }}
|
||||||
</ui-checkbox>
|
</ui-checkbox>
|
||||||
|
<ui-link
|
||||||
|
@click="appStore.setActiveState('reset-password')"
|
||||||
|
>
|
||||||
|
{{ t('forms.login.forgot') }}
|
||||||
|
</ui-link>
|
||||||
<ui-button
|
<ui-button
|
||||||
class="form__button"
|
class="form__button"
|
||||||
:isDisabled="!isFormValid"
|
:isDisabled="!isFormValid"
|
||||||
:isLoading="loading"
|
:isLoading="loading"
|
||||||
>
|
>
|
||||||
{{ t('buttons.signIn') }}
|
{{ t('buttons.login') }}
|
||||||
</ui-button>
|
</ui-button>
|
||||||
|
<p class="form__register">
|
||||||
|
{{ t('forms.login.register') }}
|
||||||
|
<ui-link
|
||||||
|
@click="appStore.setActiveState('register')"
|
||||||
|
>
|
||||||
|
{{ t('forms.register.title') }}
|
||||||
|
</ui-link>
|
||||||
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -35,8 +49,11 @@ import UiInput from "@/components/ui/ui-input.vue";
|
||||||
import UiButton from "@/components/ui/ui-button.vue";
|
import UiButton from "@/components/ui/ui-button.vue";
|
||||||
import UiCheckbox from "@/components/ui/ui-checkbox.vue";
|
import UiCheckbox from "@/components/ui/ui-checkbox.vue";
|
||||||
import {useLogin} from "@/composables/auth";
|
import {useLogin} from "@/composables/auth";
|
||||||
|
import UiLink from "@/components/ui/ui-link.vue";
|
||||||
|
import {useAppStore} from "@/stores/app.js";
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
const email = ref('')
|
const email = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
|
|
@ -61,5 +78,18 @@ async function handleLogin() {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 36px;
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__register {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="handleReset()" class="form">
|
<form @submit.prevent="handleReset()" class="form">
|
||||||
|
<h2 class="form__title">{{ t('forms.newPassword.title') }}</h2>
|
||||||
<ui-input
|
<ui-input
|
||||||
:type="'password'"
|
:type="'password'"
|
||||||
:placeholder="t('fields.newPassword')"
|
:placeholder="t('fields.newPassword')"
|
||||||
|
|
@ -62,5 +63,10 @@ async function handleReset() {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 36px;
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="handleRegister()" class="form">
|
<form @submit.prevent="handleRegister()" class="form">
|
||||||
|
<h2 class="form__title">{{ t('forms.register.title') }}</h2>
|
||||||
|
<div class="form__box">
|
||||||
<ui-input
|
<ui-input
|
||||||
:type="'text'"
|
:type="'text'"
|
||||||
:placeholder="t('fields.firstName')"
|
:placeholder="t('fields.firstName')"
|
||||||
|
|
@ -12,6 +14,8 @@
|
||||||
:rules="[required]"
|
:rules="[required]"
|
||||||
v-model="lastName"
|
v-model="lastName"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form__box">
|
||||||
<ui-input
|
<ui-input
|
||||||
:type="'text'"
|
:type="'text'"
|
||||||
:placeholder="t('fields.phoneNumber')"
|
:placeholder="t('fields.phoneNumber')"
|
||||||
|
|
@ -24,6 +28,7 @@
|
||||||
:rules="[isEmail]"
|
:rules="[isEmail]"
|
||||||
v-model="email"
|
v-model="email"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<ui-input
|
<ui-input
|
||||||
:type="'password'"
|
:type="'password'"
|
||||||
:placeholder="t('fields.password')"
|
:placeholder="t('fields.password')"
|
||||||
|
|
@ -41,8 +46,16 @@
|
||||||
:isDisabled="!isFormValid"
|
:isDisabled="!isFormValid"
|
||||||
:isLoading="loading"
|
:isLoading="loading"
|
||||||
>
|
>
|
||||||
{{ t('buttons.signUp') }}
|
{{ t('buttons.register') }}
|
||||||
</ui-button>
|
</ui-button>
|
||||||
|
<p class="form__login">
|
||||||
|
{{ t('forms.register.login') }}
|
||||||
|
<ui-link
|
||||||
|
@click="appStore.setActiveState('login')"
|
||||||
|
>
|
||||||
|
{{ t('forms.login.title') }}
|
||||||
|
</ui-link>
|
||||||
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -53,8 +66,11 @@ import {computed, ref} from "vue";
|
||||||
import UiInput from "@/components/ui/ui-input.vue";
|
import UiInput from "@/components/ui/ui-input.vue";
|
||||||
import UiButton from "@/components/ui/ui-button.vue";
|
import UiButton from "@/components/ui/ui-button.vue";
|
||||||
import {useRegister} from "@/composables/auth/index.js";
|
import {useRegister} from "@/composables/auth/index.js";
|
||||||
|
import UiLink from "@/components/ui/ui-link.vue";
|
||||||
|
import {useAppStore} from "@/stores/app.js";
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
const firstName = ref('')
|
const firstName = ref('')
|
||||||
const lastName = ref('')
|
const lastName = ref('')
|
||||||
|
|
@ -98,5 +114,24 @@ async function handleRegister() {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 36px;
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__login {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="handleReset()" class="form">
|
<form @submit.prevent="handleReset()" class="form">
|
||||||
|
<h2 class="form__title">{{ t('forms.reset.title') }}</h2>
|
||||||
<ui-input
|
<ui-input
|
||||||
:type="'email'"
|
:type="'email'"
|
||||||
:placeholder="t('fields.email')"
|
:placeholder="t('fields.email')"
|
||||||
|
|
@ -46,5 +47,10 @@ async function handleReset() {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 36px;
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -51,16 +51,16 @@ import {isEmail, isPasswordValid, required} from "@/core/rules/textFieldRules.js
|
||||||
import {computed, ref, watchEffect} from "vue";
|
import {computed, ref, watchEffect} from "vue";
|
||||||
import UiInput from "@/components/ui/ui-input.vue";
|
import UiInput from "@/components/ui/ui-input.vue";
|
||||||
import UiButton from "@/components/ui/ui-button.vue";
|
import UiButton from "@/components/ui/ui-button.vue";
|
||||||
import {useAuthStore} from "@/stores/auth.js";
|
import {useUserStore} from "@/stores/user.js";
|
||||||
import {useUserUpdating} from "@/composables/user";
|
import {useUserUpdating} from "@/composables/user";
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
const authStore = useAuthStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const userFirstName = computed(() => authStore.user?.firstName)
|
const userFirstName = computed(() => userStore.user?.firstName)
|
||||||
const userLastName = computed(() => authStore.user?.lastName)
|
const userLastName = computed(() => userStore.user?.lastName)
|
||||||
const userEmail = computed(() => authStore.user?.email)
|
const userEmail = computed(() => userStore.user?.email)
|
||||||
const userPhoneNumber = computed(() => authStore.user?.phoneNumber)
|
const userPhoneNumber = computed(() => userStore.user?.phoneNumber)
|
||||||
|
|
||||||
const firstName = ref('')
|
const firstName = ref('')
|
||||||
const lastName = ref('')
|
const lastName = ref('')
|
||||||
|
|
|
||||||
48
storefront/src/components/home/home-brands.vue
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<div class="brands">
|
||||||
|
<div class="container">
|
||||||
|
<div class="brands__wrapper">
|
||||||
|
<vue-marquee-slider
|
||||||
|
id="marquee-slider"
|
||||||
|
:speed="40000"
|
||||||
|
:paused="isMarqueePaused"
|
||||||
|
@mouseenter="isMarqueePaused = true"
|
||||||
|
@mouseleave="isMarqueePaused = false"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="brands__item"
|
||||||
|
v-for="brand in brands"
|
||||||
|
:key="brand.node.uuid"
|
||||||
|
>
|
||||||
|
<p>{{ brand.node.name }}</p>
|
||||||
|
</div>
|
||||||
|
</vue-marquee-slider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { VueMarqueeSlider } from 'vue3-marquee-slider';
|
||||||
|
import '../../../node_modules/vue3-marquee-slider/dist/style.css'
|
||||||
|
import {onMounted, ref} from "vue";
|
||||||
|
import {useBrands} from "@/composables/brands/index.js";
|
||||||
|
|
||||||
|
const isMarqueePaused = ref(false)
|
||||||
|
|
||||||
|
const { brands, loading, getBrands } = useBrands();
|
||||||
|
|
||||||
|
onMounted( async () => {
|
||||||
|
await getBrands()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.brands {
|
||||||
|
&__item {
|
||||||
|
margin-right: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
106
storefront/src/components/home/home-collection-inner.vue
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<div class="tag">
|
||||||
|
<h2 class="tag__title">{{ tag.name }}</h2>
|
||||||
|
<div class="tag__block">
|
||||||
|
<div class="tag__block-inner">
|
||||||
|
<swiper
|
||||||
|
class="swiper"
|
||||||
|
:effect="'cards'"
|
||||||
|
:grabCursor="true"
|
||||||
|
:modules="[EffectCards, Mousewheel]"
|
||||||
|
:cardsEffect="{
|
||||||
|
slideShadows: false
|
||||||
|
}"
|
||||||
|
:mousewheel="true"
|
||||||
|
>
|
||||||
|
<swiper-slide
|
||||||
|
class="swiper__slide"
|
||||||
|
v-for="product in tag.productSet.edges"
|
||||||
|
:key="product.node.uuid"
|
||||||
|
>
|
||||||
|
<product-card
|
||||||
|
:product="product.node"
|
||||||
|
/>
|
||||||
|
</swiper-slide>
|
||||||
|
</swiper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/vue';
|
||||||
|
import { EffectCards, Mousewheel } from 'swiper/modules';
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/scrollbar';
|
||||||
|
import ProductCard from "@/components/cards/product-card.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tag: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
const swiperOptions = {
|
||||||
|
speed: 500,
|
||||||
|
spaceBetween: 30,
|
||||||
|
slidesPerView: 3,
|
||||||
|
scrollbar: {
|
||||||
|
hide: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tag {
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: $accent;
|
||||||
|
font-size: 56px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__block {
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
background-color: $accentLight;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
&-inner {
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
border: 5px solid $white;
|
||||||
|
padding-inline:20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper {
|
||||||
|
width: 100%;
|
||||||
|
padding-block: 30px;
|
||||||
|
|
||||||
|
&__slide {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
|
||||||
|
& .card {
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
inset: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.swiper-slide-active) {
|
||||||
|
& .card:after {
|
||||||
|
background-color: transparent;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
104
storefront/src/components/home/home-collection.vue
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<div class="collection">
|
||||||
|
<div class="container">
|
||||||
|
<div class="collection__wrapper">
|
||||||
|
<h2 class="collection__title">{{ t('home.collection.title') }}</h2>
|
||||||
|
<div class="collection__inner">
|
||||||
|
<home-collection-inner
|
||||||
|
v-for="tag in tags"
|
||||||
|
:key="tag.uuid"
|
||||||
|
:tag="tag.node"
|
||||||
|
/>
|
||||||
|
<home-collection-inner
|
||||||
|
v-if="newProducts.length > 0"
|
||||||
|
:tag="newProductsTag"
|
||||||
|
/>
|
||||||
|
<home-collection-inner
|
||||||
|
v-if="priceProducts.length > 0"
|
||||||
|
:tag="priceProductsTag"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import HomeCollectionInner from "@/components/home/home-collection-inner.vue";
|
||||||
|
import {useProducts, useProductTags} from "@/composables/products";
|
||||||
|
import {computed, onMounted} from "vue";
|
||||||
|
|
||||||
|
const {t} = useI18n()
|
||||||
|
|
||||||
|
const { tags, loading: tagsLoading, getProductTags } = useProductTags();
|
||||||
|
const {
|
||||||
|
products: newProducts,
|
||||||
|
loading: newLoading,
|
||||||
|
getProducts: getNewProducts
|
||||||
|
} = useProducts();
|
||||||
|
|
||||||
|
const {
|
||||||
|
products: priceProducts,
|
||||||
|
loading: priceLoading,
|
||||||
|
getProducts: getPriceProducts
|
||||||
|
} = useProducts();
|
||||||
|
|
||||||
|
const newProductsTag = computed(() => {
|
||||||
|
return {
|
||||||
|
name: t('home.collection.newTag'),
|
||||||
|
uuid: 'new-products',
|
||||||
|
productSet: {
|
||||||
|
edges: newProducts.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const priceProductsTag = computed(() => {
|
||||||
|
return {
|
||||||
|
name: t('home.collection.cheapTag'),
|
||||||
|
uuid: 'price-products',
|
||||||
|
productSet: {
|
||||||
|
edges: priceProducts.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted( async () => {
|
||||||
|
await getProductTags()
|
||||||
|
await Promise.all([
|
||||||
|
getProductTags(),
|
||||||
|
getNewProducts({
|
||||||
|
orderBy: '-modified'
|
||||||
|
}),
|
||||||
|
getPriceProducts({
|
||||||
|
orderBy: '-price'
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.collection {
|
||||||
|
&__wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 72px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #dd6878;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
42
storefront/src/components/home/home-hero.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div class="hero">
|
||||||
|
<div class="container">
|
||||||
|
<div class="hero__wrapper">
|
||||||
|
<img src="@images/evibes-big.png" alt="logo">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hero {
|
||||||
|
background-image: url(@images/homeBg.png);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
-webkit-background-size: cover;
|
||||||
|
background-size: cover;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba($black, 0.5);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding-block: 100px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<el-skeleton class="sk" animated>
|
||||||
|
<template #template>
|
||||||
|
<el-skeleton-item
|
||||||
|
variant="p"
|
||||||
|
class="sk__text"
|
||||||
|
v-for="idx in 3"
|
||||||
|
:key="idx"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-skeleton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sk {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
storefront/src/components/skeletons/ui/counter-skeleton.vue
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<el-skeleton class="sk" animated>
|
||||||
|
<template #template>
|
||||||
|
<el-skeleton-item
|
||||||
|
variant="p"
|
||||||
|
class="sk__text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-skeleton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sk {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute !important;
|
||||||
|
top: -10px;
|
||||||
|
right: -15px;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<el-skeleton class="sk" animated>
|
||||||
|
<template #template>
|
||||||
|
<el-skeleton-item
|
||||||
|
variant="p"
|
||||||
|
class="sk__text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-skeleton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sk {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -26,32 +26,36 @@ const props = defineProps({
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
border: 1px solid $black;
|
border: 1px solid $accent;
|
||||||
background-color: $white;
|
background-color: $accent;
|
||||||
padding-block: 5px;
|
border-radius: $default_border_radius;
|
||||||
|
padding-block: 7px;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
color: $black;
|
color: $white;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
||||||
&:hover, &.active {
|
@include hover {
|
||||||
background-color: $black;
|
background-color: $accentLight;
|
||||||
color: $white;
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $accentLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
background-color: rgba($black, 0.5);
|
background-color: $accentDisabled;
|
||||||
color: $black;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled:hover, &.active {
|
&:disabled:hover, &.active {
|
||||||
background-color: rgba($black, 0.5);
|
background-color: $accentDisabled;
|
||||||
color: $black;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__loader {
|
&__loader {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="checkbox">
|
||||||
<input
|
<input
|
||||||
:id="'checkbox' + id"
|
:id="id ? `checkbox + id` : 'checkbox'"
|
||||||
class="checkbox"
|
class="checkbox__input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
:checked="modelValue"
|
:checked="modelValue"
|
||||||
>
|
>
|
||||||
<label :for="'checkbox' + id" class="checkbox__label">
|
<span class="checkbox__block" @click="toggleCheckbox"></span>
|
||||||
|
<label :for="id ? `checkbox + id` : 'checkbox'" class="checkbox__label">
|
||||||
<slot />
|
<slot />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -24,44 +25,56 @@ const props = defineProps({
|
||||||
const onInput = (event) => {
|
const onInput = (event) => {
|
||||||
$emit('update:modelValue', event.target.checked);
|
$emit('update:modelValue', event.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleCheckbox = () => {
|
||||||
|
$emit('update:modelValue', !props.modelValue);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.checkbox {
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
&__input {
|
||||||
display: none;
|
display: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__block {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid $black;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background-color: $accent;
|
||||||
|
border-radius: 2px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
|
cursor: pointer;
|
||||||
color: #2B2B2B;
|
color: #2B2B2B;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
letter-spacing: 0.12px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox + .checkbox__label::before {
|
.checkbox__input:checked + .checkbox__block::after {
|
||||||
content: '';
|
opacity: 1;
|
||||||
display: inline-block;
|
|
||||||
width: 17px;
|
|
||||||
height: 17px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
border: 1px solid $black;
|
|
||||||
margin-right: 10px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center center;
|
|
||||||
background-size: 50% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox:checked + .checkbox__label::before {
|
|
||||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2f6b4f' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e");
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox + .checkbox__label {
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
27
storefront/src/components/ui/ui-counter.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<div class="counter">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.counter {
|
||||||
|
position: absolute !important;
|
||||||
|
top: -10px;
|
||||||
|
right: -15px;
|
||||||
|
background-color: $accent;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -12,10 +12,10 @@
|
||||||
<button
|
<button
|
||||||
@click.prevent="setPasswordVisible"
|
@click.prevent="setPasswordVisible"
|
||||||
class="block__eyes"
|
class="block__eyes"
|
||||||
v-if="type === 'password'"
|
v-if="type === 'password' && modelValue"
|
||||||
>
|
>
|
||||||
<img v-if="isPasswordVisible === 'password'" src="@icons/eyeClosed.svg" alt="eye" loading="lazy">
|
<i v-if="isPasswordVisible === 'password'" class="pi pi-eye-slash"></i>
|
||||||
<img v-else src="@icons/eyeOpened.svg" alt="eye" loading="lazy">
|
<i v-else class="pi pi-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!validate" class="block__error">{{ errorMessage }}</p>
|
<p v-if="!validate" class="block__error">{{ errorMessage }}</p>
|
||||||
|
|
@ -99,7 +99,9 @@ const onInput = (e) => {
|
||||||
&__input {
|
&__input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border: 1px solid $black;
|
border: 1px solid #e0e0e0;
|
||||||
|
//border: 1px solid #b2b2b2;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
|
|
||||||
color: #1f1f1f;
|
color: #1f1f1f;
|
||||||
|
|
@ -121,6 +123,8 @@ const onInput = (e) => {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #838383;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__error {
|
&__error {
|
||||||
|
|
|
||||||
125
storefront/src/components/ui/ui-language-switcher.vue
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
<template>
|
||||||
|
<div class="switcher" ref="switcherRef">
|
||||||
|
<div
|
||||||
|
@click="setSwitcherVisible(!isSwitcherVisible)"
|
||||||
|
class="switcher__button"
|
||||||
|
:class="[{ active: isSwitcherVisible }]"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="currentLocale"
|
||||||
|
:src="currentLocale.flag"
|
||||||
|
:alt="currentLocale.code"
|
||||||
|
>
|
||||||
|
<language-switcher-skeleton v-else />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="switcher__menu"
|
||||||
|
:class="[{active: isSwitcherVisible}]"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="switcher__menu-button"
|
||||||
|
v-for="locale of locales"
|
||||||
|
:key="locale.code"
|
||||||
|
@click="switchLanguage(locale.code)"
|
||||||
|
:src="locale.flag"
|
||||||
|
:alt="locale.code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {onClickOutside} from "@vueuse/core";
|
||||||
|
import {useLanguageStore} from "@/stores/languages.js";
|
||||||
|
import {useLanguageSwitch} from "@/composables/languages/index.js";
|
||||||
|
import LanguageSwitcherSkeleton from "@/components/skeletons/ui/language-switcher-skeleton.vue";
|
||||||
|
|
||||||
|
const languageStore = useLanguageStore()
|
||||||
|
|
||||||
|
const locales = computed(() => languageStore.languages)
|
||||||
|
const currentLocale = computed(() => languageStore.currentLocale)
|
||||||
|
|
||||||
|
const isSwitcherVisible = ref(false)
|
||||||
|
const setSwitcherVisible = (state) => {
|
||||||
|
isSwitcherVisible.value = state
|
||||||
|
}
|
||||||
|
|
||||||
|
const switcherRef = ref(null)
|
||||||
|
onClickOutside(switcherRef, () => isSwitcherVisible.value = false)
|
||||||
|
|
||||||
|
const { switchLanguage } = useLanguageSwitch()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.switcher {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 52px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
border: 1px solid $accent;
|
||||||
|
background-color: #ddd9ef;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
& img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 110%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border: 0 solid $accent;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
max-height: 1000px;
|
||||||
|
border: 1px solid $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 8px;
|
||||||
|
background-color: #ddd9ef;
|
||||||
|
transition: 0.1s;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
42
storefront/src/components/ui/ui-link.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div @click="redirect" class="link">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
import {DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY} from "@/config/index.js";
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
routeName: String
|
||||||
|
})
|
||||||
|
|
||||||
|
const redirect = () => {
|
||||||
|
if (props.routeName) {
|
||||||
|
router.push({
|
||||||
|
name: props.routeName,
|
||||||
|
params: {
|
||||||
|
locale: localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.link {
|
||||||
|
width: fit-content;
|
||||||
|
transition: 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $accent;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
color: #5539ce;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -3,13 +3,12 @@ import {LOGIN} from "@/graphql/mutations/auth.js";
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import {ElNotification} from "element-plus";
|
import {ElNotification} from "element-plus";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useAuthStore} from "@/stores/auth.js";
|
import {useUserStore} from "@/stores/user.js";
|
||||||
import translations from "@/core/helpers/translations.js";
|
import translations from "@/core/helpers/translations.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LOCALE,
|
DEFAULT_LOCALE, LOCALE_STORAGE_ACCESS_TOKEN_KEY,
|
||||||
LOCALE_STORAGE_LOCALE_KEY,
|
LOCALE_STORAGE_LOCALE_KEY,
|
||||||
LOCALE_STORAGE_REFRESH_KEY,
|
LOCALE_STORAGE_REFRESH_TOKEN_KEY,
|
||||||
LOCALE_STORAGE_STAY_LOGIN_KEY
|
|
||||||
} from "@/config/index.js";
|
} from "@/config/index.js";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {usePendingOrder} from "@/composables/orders";
|
import {usePendingOrder} from "@/composables/orders";
|
||||||
|
|
@ -18,7 +17,7 @@ import {useWishlist} from "@/composables/wishlist";
|
||||||
export function useLogin() {
|
export function useLogin() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const authStore = useAuthStore()
|
const userStore = useUserStore()
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
const { mutate: loginMutation } = useMutation(LOGIN);
|
const { mutate: loginMutation } = useMutation(LOGIN);
|
||||||
|
|
@ -42,16 +41,14 @@ export function useLogin() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isStayLogin) {
|
if (isStayLogin) {
|
||||||
localStorage.setItem(LOCALE_STORAGE_STAY_LOGIN_KEY, 'remember')
|
localStorage.setItem(LOCALE_STORAGE_REFRESH_TOKEN_KEY, response.data.obtainJwtToken.refreshToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.data?.obtainJwtToken) {
|
if (response.data?.obtainJwtToken) {
|
||||||
authStore.setUser({
|
userStore.setUser({
|
||||||
user: response.data.obtainJwtToken.user,
|
user: response.data.obtainJwtToken.user
|
||||||
accessToken: response.data.obtainJwtToken.accessToken
|
|
||||||
});
|
});
|
||||||
|
localStorage.setItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY, response.data.obtainJwtToken.accessToken)
|
||||||
localStorage.setItem(LOCALE_STORAGE_REFRESH_KEY, response.data.obtainJwtToken.refreshToken)
|
|
||||||
|
|
||||||
ElNotification({
|
ElNotification({
|
||||||
message: t('popup.success.login'),
|
message: t('popup.success.login'),
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,23 @@
|
||||||
import {useAuthStore} from "@/stores/auth.js";
|
import {useUserStore} from "@/stores/user.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LOCALE,
|
DEFAULT_LOCALE, LOCALE_STORAGE_ACCESS_TOKEN_KEY,
|
||||||
LOCALE_STORAGE_LOCALE_KEY,
|
LOCALE_STORAGE_LOCALE_KEY,
|
||||||
LOCALE_STORAGE_REFRESH_KEY,
|
LOCALE_STORAGE_REFRESH_TOKEN_KEY
|
||||||
LOCALE_STORAGE_STAY_LOGIN_KEY
|
|
||||||
} from "@/config/index.js";
|
} from "@/config/index.js";
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
|
|
||||||
export function useLogout() {
|
export function useLogout() {
|
||||||
const authStore = useAuthStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
authStore.setUser({
|
userStore.setUser({
|
||||||
user: null,
|
user: null,
|
||||||
accessToken: null
|
accessToken: null
|
||||||
})
|
})
|
||||||
|
|
||||||
localStorage.removeItem(LOCALE_STORAGE_REFRESH_KEY)
|
localStorage.removeItem(LOCALE_STORAGE_REFRESH_TOKEN_KEY)
|
||||||
localStorage.removeItem(LOCALE_STORAGE_STAY_LOGIN_KEY)
|
localStorage.removeItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY)
|
||||||
|
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'home',
|
name: 'home',
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import {REFRESH} from "@/graphql/mutations/auth.js";
|
||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
import {ElNotification} from "element-plus";
|
import {ElNotification} from "element-plus";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useAuthStore} from "@/stores/auth.js";
|
import {useUserStore} from "@/stores/user.js";
|
||||||
import {LOCALE_STORAGE_REFRESH_KEY} from "@/config/index.js";
|
import {LOCALE_STORAGE_REFRESH_TOKEN_KEY} from "@/config/index.js";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import translations from "@/core/helpers/translations.js";
|
import translations from "@/core/helpers/translations.js";
|
||||||
import {usePendingOrder} from "@/composables/orders";
|
import {usePendingOrder} from "@/composables/orders";
|
||||||
|
|
@ -13,7 +13,7 @@ import {useWishlist} from "@/composables/wishlist";
|
||||||
export function useRefresh() {
|
export function useRefresh() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const authStore = useAuthStore()
|
const userStore = useUserStore()
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
const { mutate: refreshMutation } = useMutation(REFRESH);
|
const { mutate: refreshMutation } = useMutation(REFRESH);
|
||||||
|
|
@ -26,7 +26,7 @@ export function useRefresh() {
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
const refreshToken = computed(() => localStorage.getItem(LOCALE_STORAGE_REFRESH_KEY))
|
const refreshToken = computed(() => localStorage.getItem(LOCALE_STORAGE_REFRESH_TOKEN_KEY))
|
||||||
|
|
||||||
if (!refreshToken.value) return
|
if (!refreshToken.value) return
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ export function useRefresh() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data?.refreshJwtToken) {
|
if (response.data?.refreshJwtToken) {
|
||||||
authStore.setUser({
|
userStore.setUser({
|
||||||
user: response.data.refreshJwtToken.user,
|
user: response.data.refreshJwtToken.user,
|
||||||
accessToken: response.data.refreshJwtToken.accessToken
|
accessToken: response.data.refreshJwtToken.accessToken
|
||||||
})
|
})
|
||||||
|
|
@ -45,7 +45,7 @@ export function useRefresh() {
|
||||||
translations.switchLanguage(response.data.refreshJwtToken.user.language, router, route)
|
translations.switchLanguage(response.data.refreshJwtToken.user.language, router, route)
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(LOCALE_STORAGE_REFRESH_KEY, response.data.refreshJwtToken.refreshToken)
|
localStorage.setItem(LOCALE_STORAGE_REFRESH_TOKEN_KEY, response.data.refreshJwtToken.refreshToken)
|
||||||
|
|
||||||
await getPendingOrder(response.data.refreshJwtToken.user.email);
|
await getPendingOrder(response.data.refreshJwtToken.user.email);
|
||||||
await getWishlist();
|
await getWishlist();
|
||||||
|
|
|
||||||
2
storefront/src/composables/brands/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './useBrands'
|
||||||
|
export * from './useBrandByUuid'
|
||||||
24
storefront/src/composables/brands/useBrandByUuid.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { useLazyQuery } from "@vue/apollo-composable";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {GET_BRAND_BY_UUID} from "@/graphql/queries/brands.js";
|
||||||
|
|
||||||
|
export function useBrandByUuid() {
|
||||||
|
const { result, loading, error, load } = useLazyQuery(GET_BRAND_BY_UUID);
|
||||||
|
|
||||||
|
const brand = computed(() => result.value?.brands.edges[0].node ?? []);
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error("usePostbySlug error:", error.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBrand = (uuid) => {
|
||||||
|
return load(null, { uuid });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
brand,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
getBrand
|
||||||
|
};
|
||||||
|
}
|
||||||
20
storefront/src/composables/brands/useBrands.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useLazyQuery } from "@vue/apollo-composable";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {GET_BRANDS} from "@/graphql/queries/brands.js";
|
||||||
|
|
||||||
|
export function useBrands() {
|
||||||
|
const { result, loading, error, load } = useLazyQuery(GET_BRANDS);
|
||||||
|
|
||||||
|
const brands = computed(() => result.value?.brands.edges ?? []);
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error("useBrands error:", error.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
brands,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
getBrands: load
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,13 @@ import { useLazyQuery } from "@vue/apollo-composable";
|
||||||
import {computed} from "vue";
|
import {computed} from "vue";
|
||||||
import {GET_CATEGORY_BY_SLUG} from "@/graphql/queries/categories.js";
|
import {GET_CATEGORY_BY_SLUG} from "@/graphql/queries/categories.js";
|
||||||
|
|
||||||
export function usePostbySlug() {
|
export function useCategorybySlug() {
|
||||||
const { result, loading, error, load } = useLazyQuery(GET_CATEGORY_BY_SLUG);
|
const { result, loading, error, load } = useLazyQuery(GET_CATEGORY_BY_SLUG);
|
||||||
|
|
||||||
const category = computed(() => result.value?.categories.edges[0].node ?? []);
|
const category = computed(() => result.value?.categories.edges[0].node ?? []);
|
||||||
|
|
||||||
if (error.value) {
|
if (error.value) {
|
||||||
console.error("usePostbySlug error:", error.value);
|
console.error("useCategorybySlug error:", error.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCategory = (slug) => {
|
const getCategory = (slug) => {
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,19 @@ import {useMutation} from "@vue/apollo-composable";
|
||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
import {ElNotification} from "element-plus";
|
import {ElNotification} from "element-plus";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useAuthStore} from "@/stores/auth.js";
|
import {useUserStore} from "@/stores/user.js";
|
||||||
import translations from "@/core/helpers/translations.js";
|
import translations from "@/core/helpers/translations.js";
|
||||||
import {SWITCH_LANGUAGE} from "@/graphql/mutations/languages.js";
|
import {SWITCH_LANGUAGE} from "@/graphql/mutations/languages.js";
|
||||||
|
import {LOCALE_STORAGE_ACCESS_TOKEN_KEY} from "@/config/index.js";
|
||||||
|
|
||||||
export function useLanguageSwitch() {
|
export function useLanguageSwitch() {
|
||||||
const authStore = useAuthStore()
|
const userStore = useUserStore()
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
const { mutate: languageSwitchMutation } = useMutation(SWITCH_LANGUAGE);
|
const { mutate: languageSwitchMutation } = useMutation(SWITCH_LANGUAGE);
|
||||||
|
|
||||||
const accessToken = computed(() => authStore.accessToken)
|
const accessToken = computed(() => localStorage.getItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY))
|
||||||
const userUuid = computed(() => authStore.user?.uuid)
|
const userUuid = computed(() => userStore.user?.uuid)
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
|
@ -31,7 +32,7 @@ export function useLanguageSwitch() {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data?.updateUser) {
|
if (response.data?.updateUser) {
|
||||||
authStore.setUser({
|
userStore.setUser({
|
||||||
user: response.data.updateUser.user,
|
user: response.data.updateUser.user,
|
||||||
accessToken: accessToken.value
|
accessToken: accessToken.value
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useLazyQuery } from "@vue/apollo-composable";
|
||||||
import {watchEffect} from "vue";
|
import {watchEffect} from "vue";
|
||||||
import {GET_LANGUAGES} from "@/graphql/queries/languages.js";
|
import {GET_LANGUAGES} from "@/graphql/queries/languages.js";
|
||||||
import {useLanguageStore} from "@/stores/languages.js";
|
import {useLanguageStore} from "@/stores/languages.js";
|
||||||
import {SUPPORTED_LOCALES} from "@/config/index.js";
|
import {LOCALE_STORAGE_LOCALE_KEY, SUPPORTED_LOCALES} from "@/config/index.js";
|
||||||
|
|
||||||
export function useLanguages() {
|
export function useLanguages() {
|
||||||
const languageStore = useLanguageStore()
|
const languageStore = useLanguageStore()
|
||||||
|
|
@ -22,6 +22,8 @@ export function useLanguages() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
languageStore.setCurrentLocale(languageStore.languages.find((locale) => locale.code === localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY)))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
export * from './useProducts'
|
export * from './useProducts'
|
||||||
|
export * from './useProductBySlug'
|
||||||
|
export * from './useProductTags'
|
||||||
20
storefront/src/composables/products/useProductTags.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useLazyQuery } from "@vue/apollo-composable";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {GET_PRODUCT_BY_SLUG, GET_PRODUCT_TAGS} from "@/graphql/queries/products.js";
|
||||||
|
|
||||||
|
export function useProductTags() {
|
||||||
|
const { result, loading, error, load } = useLazyQuery(GET_PRODUCT_TAGS);
|
||||||
|
|
||||||
|
const tags = computed(() => result.value?.productTags.edges ?? []);
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error("useProductTags error:", error.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
getProductTags: load
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,46 +1,39 @@
|
||||||
import { ref } from 'vue';
|
import { useLazyQuery } from "@vue/apollo-composable";
|
||||||
import { useQuery } from '@vue/apollo-composable';
|
import { computed, ref } from "vue";
|
||||||
import { GET_PRODUCTS } from "@/graphql/queries/products.js";
|
import { GET_PRODUCTS } from "@/graphql/queries/products.js";
|
||||||
|
|
||||||
export function useProducts() {
|
export function useProducts() {
|
||||||
const products = ref([]);
|
const variables = ref({
|
||||||
const pageInfo = ref([]);
|
first: 12
|
||||||
const loading = ref(false);
|
});
|
||||||
|
|
||||||
|
const { result, loading, error, load } = useLazyQuery(
|
||||||
|
GET_PRODUCTS,
|
||||||
|
() => variables.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const products = computed(() => result.value?.products.edges ?? []);
|
||||||
|
const pageInfo = computed(() => result.value?.products.pageInfo ?? {});
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error("useProducts error:", error.value);
|
||||||
|
}
|
||||||
|
|
||||||
const getProducts = async (params = {}) => {
|
const getProducts = async (params = {}) => {
|
||||||
loading.value = true;
|
const newVariables = { ...variables.value };
|
||||||
|
|
||||||
const defaults = {
|
|
||||||
first: 12
|
|
||||||
};
|
|
||||||
|
|
||||||
const variables = {};
|
|
||||||
|
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
if (value !== undefined && value !== null && value !== '') {
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
variables[key] = value;
|
newVariables[key] = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.entries(defaults).forEach(([key, value]) => {
|
variables.value = newVariables;
|
||||||
if (!(key in variables)) {
|
|
||||||
variables[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
if (result.value) {
|
||||||
const { onResult } = useQuery(GET_PRODUCTS, variables);
|
await refetch();
|
||||||
|
} else {
|
||||||
onResult(result => {
|
await load();
|
||||||
if (result.data && result.data.products) {
|
|
||||||
products.value = result.data.products.edges;
|
|
||||||
pageInfo.value = result.data.products.pageInfo;
|
|
||||||
}
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('useProducts error:', error);
|
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -48,6 +41,7 @@ export function useProducts() {
|
||||||
products,
|
products,
|
||||||
pageInfo,
|
pageInfo,
|
||||||
loading,
|
loading,
|
||||||
|
error,
|
||||||
getProducts
|
getProducts
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
2
storefront/src/composables/search/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './useSearch'
|
||||||
|
export * from './useSearchUi'
|
||||||
52
storefront/src/composables/search/useSearch.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import {useMutation} from "@vue/apollo-composable";
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {ElNotification} from "element-plus";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import {SEARCH} from "@/graphql/mutations/search.js";
|
||||||
|
|
||||||
|
export function useSearch() {
|
||||||
|
const {t} = useI18n();
|
||||||
|
|
||||||
|
const { mutate: searchMutation } = useMutation(SEARCH);
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const searchResults = ref(null);
|
||||||
|
|
||||||
|
async function search(
|
||||||
|
query
|
||||||
|
) {
|
||||||
|
loading.value = true;
|
||||||
|
searchResults.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await searchMutation({
|
||||||
|
query
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data?.search) {
|
||||||
|
searchResults.value = response.data.search.results;
|
||||||
|
return response.data.search;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("useSearch error:", error);
|
||||||
|
|
||||||
|
const errorMessage = error.graphQLErrors?.[0]?.message ||
|
||||||
|
error.message ||
|
||||||
|
t('popup.errors.defaultError');
|
||||||
|
|
||||||
|
ElNotification({
|
||||||
|
title: t('popup.errors.main'),
|
||||||
|
message: errorMessage,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
search,
|
||||||
|
loading,
|
||||||
|
searchResults
|
||||||
|
};
|
||||||
|
}
|
||||||
67
storefront/src/composables/search/useSearchUi.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useSearch } from './useSearch.js';
|
||||||
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
|
|
||||||
|
export function useSearchUI() {
|
||||||
|
const query = ref('');
|
||||||
|
const isSearchActive = ref(false);
|
||||||
|
const { search, loading, searchResults } = useSearch();
|
||||||
|
|
||||||
|
const filteredSearchResults = computed(() => {
|
||||||
|
if (!searchResults.value) return {};
|
||||||
|
|
||||||
|
return Object.entries(searchResults.value)
|
||||||
|
.reduce((acc, [category, blocks]) => {
|
||||||
|
if (blocks.length > 0) {
|
||||||
|
acc[category] = blocks;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasResults = computed(() => {
|
||||||
|
if (!searchResults.value) return false;
|
||||||
|
|
||||||
|
return Object.keys(searchResults.value).some(key => {
|
||||||
|
return Array.isArray(searchResults.value[key]) &&
|
||||||
|
searchResults.value[key].length > 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getBlockTitle(category) {
|
||||||
|
return category.charAt(0).toUpperCase() + category.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
query.value = '';
|
||||||
|
searchResults.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSearch(value) {
|
||||||
|
isSearchActive.value = value !== undefined ? value : !isSearchActive.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedSearch = useDebounceFn(async () => {
|
||||||
|
if (query.value) {
|
||||||
|
await search(query.value);
|
||||||
|
} else {
|
||||||
|
searchResults.value = null;
|
||||||
|
}
|
||||||
|
}, 750);
|
||||||
|
|
||||||
|
watch(() => query.value, async () => {
|
||||||
|
await debouncedSearch();
|
||||||
|
}, { immediate: false });
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
isSearchActive,
|
||||||
|
loading,
|
||||||
|
searchResults,
|
||||||
|
filteredSearchResults,
|
||||||
|
hasResults,
|
||||||
|
getBlockTitle,
|
||||||
|
clearSearch,
|
||||||
|
toggleSearch
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,25 +2,26 @@ import {useMutation} from "@vue/apollo-composable";
|
||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
import {ElNotification} from "element-plus";
|
import {ElNotification} from "element-plus";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useAuthStore} from "@/stores/auth.js";
|
import {useUserStore} from "@/stores/user.js";
|
||||||
import translations from "@/core/helpers/translations.js";
|
import translations from "@/core/helpers/translations.js";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {useLogout} from "@/composables/auth";
|
import {useLogout} from "@/composables/auth";
|
||||||
import {UPDATE_USER} from "@/graphql/mutations/user.js";
|
import {UPDATE_USER} from "@/graphql/mutations/user.js";
|
||||||
|
import {LOCALE_STORAGE_ACCESS_TOKEN_KEY} from "@/config/index.js";
|
||||||
|
|
||||||
export function useUserUpdating() {
|
export function useUserUpdating() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const authStore = useAuthStore()
|
const userStore = useUserStore()
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
const { mutate: userUpdatingMutation } = useMutation(UPDATE_USER);
|
const { mutate: userUpdatingMutation } = useMutation(UPDATE_USER);
|
||||||
|
|
||||||
const { logout } = useLogout();
|
const { logout } = useLogout();
|
||||||
|
|
||||||
const accessToken = computed(() => authStore.accessToken)
|
const accessToken = computed(() => localStorage.getItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY))
|
||||||
const userUuid = computed(() => authStore.user?.uuid)
|
const userUuid = computed(() => userStore.user?.uuid)
|
||||||
const userEmail = computed(() => authStore.user?.email)
|
const userEmail = computed(() => userStore.user?.email)
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
|
@ -81,7 +82,7 @@ export function useUserUpdating() {
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
authStore.setUser({
|
userStore.setUser({
|
||||||
user: response.data.updateUser.user,
|
user: response.data.updateUser.user,
|
||||||
accessToken: accessToken.value
|
accessToken: accessToken.value
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,66 @@ export const SUPPORTED_LOCALES = [
|
||||||
{
|
{
|
||||||
code: 'en-gb',
|
code: 'en-gb',
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ar-ar',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'cs-cz',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'da-dk',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'de-de',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'en-us',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'es-es',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'fr-fr',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'it-it',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ja-jp',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'nl-nl',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'pl-pl',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'pt-br',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ro-ro',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ru-ru',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'zh-hans',
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -23,6 +83,6 @@ export const DEFAULT_LOCALE = SUPPORTED_LOCALES.find(locale => locale.default)?.
|
||||||
|
|
||||||
export const LOCALE_STORAGE_LOCALE_KEY = `${APP_NAME_KEY}-user-locale`;
|
export const LOCALE_STORAGE_LOCALE_KEY = `${APP_NAME_KEY}-user-locale`;
|
||||||
|
|
||||||
export const LOCALE_STORAGE_REFRESH_KEY = `${APP_NAME_KEY}-refresh`;
|
export const LOCALE_STORAGE_REFRESH_TOKEN_KEY = `${APP_NAME_KEY}-refresh`;
|
||||||
|
|
||||||
export const LOCALE_STORAGE_STAY_LOGIN_KEY = `${APP_NAME_KEY}-remember`;
|
export const LOCALE_STORAGE_ACCESS_TOKEN_KEY = `${APP_NAME_KEY}-access`;
|
||||||
8
storefront/src/graphql/fragments/brands.fragment.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const BRAND_FRAGMENT = gql`
|
||||||
|
fragment Brand on BrandType {
|
||||||
|
uuid
|
||||||
|
name
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import gql from 'graphql-tag'
|
|
||||||
|
|
||||||
export const COMPANY_FRAGMENT = gql`
|
|
||||||
fragment Company on ConfigType {
|
|
||||||
companyAddress
|
|
||||||
companyName
|
|
||||||
companyPhoneNumber
|
|
||||||
emailFrom
|
|
||||||
emailHostUser
|
|
||||||
projectName
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import gql from 'graphql-tag'
|
|
||||||
|
|
||||||
export const LANGUAGES_FRAGMENT = gql`
|
|
||||||
fragment Languages on LanguageType {
|
|
||||||
code
|
|
||||||
flag
|
|
||||||
name
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
@ -33,5 +33,21 @@ export const PRODUCT_FRAGMENT = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
feedbacks {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
uuid
|
||||||
|
rating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
tagName
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
34
storefront/src/graphql/mutations/search.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
|
export const SEARCH = gql`
|
||||||
|
mutation search(
|
||||||
|
$query: String!
|
||||||
|
) {
|
||||||
|
search(
|
||||||
|
query: $query
|
||||||
|
) {
|
||||||
|
results {
|
||||||
|
brands {
|
||||||
|
uuid
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
uuid
|
||||||
|
}
|
||||||
|
posts {
|
||||||
|
uuid
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
products {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
37
storefront/src/graphql/queries/brands.js
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import {BRAND_FRAGMENT} from "@/graphql/fragments/brands.fragment.js";
|
||||||
|
import {CATEGORY_FRAGMENT} from "@/graphql/fragments/categories.fragment.js";
|
||||||
|
|
||||||
|
export const GET_BRANDS = gql`
|
||||||
|
query getBrands {
|
||||||
|
brands {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Brand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${BRAND_FRAGMENT}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GET_BRAND_BY_UUID = gql`
|
||||||
|
query getBrandbyUuid(
|
||||||
|
$uuid: String!
|
||||||
|
) {
|
||||||
|
brands(
|
||||||
|
uuid: $uuid
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Brand
|
||||||
|
categories {
|
||||||
|
...Category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${BRAND_FRAGMENT}
|
||||||
|
${CATEGORY_FRAGMENT}
|
||||||
|
`
|
||||||
|
|
@ -7,6 +7,15 @@ export const GET_CATEGORIES = gql`
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
...Category
|
...Category
|
||||||
|
children {
|
||||||
|
...Category
|
||||||
|
children {
|
||||||
|
...Category
|
||||||
|
children {
|
||||||
|
...Category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import {COMPANY_FRAGMENT} from "@/graphql/fragments/company.fragment.js";
|
|
||||||
|
|
||||||
export const GET_COMPANY_INFO = gql`
|
export const GET_COMPANY_INFO = gql`
|
||||||
query getCompanyInfo {
|
query getCompanyInfo {
|
||||||
parameters {
|
parameters {
|
||||||
...Company
|
companyAddress
|
||||||
|
companyName
|
||||||
|
companyPhoneNumber
|
||||||
|
emailFrom
|
||||||
|
emailHostUser
|
||||||
|
projectName
|
||||||
|
paymentGatewayMinimum
|
||||||
|
paymentGatewayMaximum
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${COMPANY_FRAGMENT}
|
|
||||||
`
|
`
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import {LANGUAGES_FRAGMENT} from "@/graphql/fragments/languages.fragment.js";
|
|
||||||
|
|
||||||
export const GET_LANGUAGES = gql`
|
export const GET_LANGUAGES = gql`
|
||||||
query getLanguages {
|
query getLanguages {
|
||||||
languages {
|
languages {
|
||||||
...Languages
|
code
|
||||||
|
flag
|
||||||
|
name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${LANGUAGES_FRAGMENT}
|
|
||||||
`
|
`
|
||||||
|
|
@ -51,3 +51,25 @@ export const GET_PRODUCT_BY_SLUG = gql`
|
||||||
}
|
}
|
||||||
${PRODUCT_FRAGMENT}
|
${PRODUCT_FRAGMENT}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const GET_PRODUCT_TAGS = gql`
|
||||||
|
query getProductTags {
|
||||||
|
productTags {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
uuid
|
||||||
|
name
|
||||||
|
tagName
|
||||||
|
productSet {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${PRODUCT_FRAGMENT}
|
||||||
|
`
|
||||||
3
storefront/src/locales/ar-ar.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/cs-cz.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/da-dk.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/de-de.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"signIn": "Sign In",
|
"login": "Login",
|
||||||
"signUp": "Sign Up",
|
"register": "Register",
|
||||||
"addToCart": "Add To Cart",
|
"addToCart": "Add To Cart",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"goEmail": "Take me to my inbox",
|
"goEmail": "Take me to my inbox",
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
"pageNotFound": "Page not found"
|
"pageNotFound": "Page not found"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"search": "Search Cards",
|
"search": "Search",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"firstName": "First name",
|
"firstName": "First name",
|
||||||
"lastName": "Last name",
|
"lastName": "Last name",
|
||||||
|
|
@ -62,5 +62,47 @@
|
||||||
"payment": "Your purchase is being processed! Please stand by",
|
"payment": "Your purchase is being processed! Please stand by",
|
||||||
"successCheckout": "Order purchase successful!",
|
"successCheckout": "Order purchase successful!",
|
||||||
"addToWishlist": "{product} has been added to the wishlist!"
|
"addToWishlist": "{product} has been added to the wishlist!"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"actions": {
|
||||||
|
"wishlist": "Wishlist",
|
||||||
|
"cart": "Cart",
|
||||||
|
"user": "Login"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"empty": "Nothing found"
|
||||||
|
},
|
||||||
|
"catalogue": {
|
||||||
|
"title": "Catalogue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"collection": {
|
||||||
|
"title": "Our collection",
|
||||||
|
"newTag": "New",
|
||||||
|
"cheapTag": "Low-budget"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forms": {
|
||||||
|
"login": {
|
||||||
|
"title": "Login",
|
||||||
|
"forgot": "Forgot password?",
|
||||||
|
"register": "Don't have an account?"
|
||||||
|
},
|
||||||
|
"register": {
|
||||||
|
"title": "Register",
|
||||||
|
"login": "Do you have an account?"
|
||||||
|
},
|
||||||
|
"reset": {
|
||||||
|
"title": "Reset password"
|
||||||
|
},
|
||||||
|
"newPassword": {
|
||||||
|
"title": "New password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cards": {
|
||||||
|
"product": {
|
||||||
|
"stock": "In stock: "
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
3
storefront/src/locales/en-us.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/es-es.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/fr-fr.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/it-it.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/ja-jp.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/nl-nl.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/pl-pl.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/pt-br.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/ro-ro.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/ru-ru.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/src/locales/zh-hans.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import '@/assets/styles/global/fonts.scss'
|
import '@/assets/styles/global/fonts.scss'
|
||||||
import '@/assets/styles/main.scss'
|
import '@/assets/styles/main.scss'
|
||||||
|
import 'primeicons/primeicons.css'
|
||||||
import {createApp, h, provide} from 'vue'
|
import {createApp, h, provide} from 'vue'
|
||||||
import { DefaultApolloClient } from '@vue/apollo-composable'
|
import { DefaultApolloClient } from '@vue/apollo-composable'
|
||||||
import { createApolloClient } from './apollo'
|
import { createApolloClient } from './apollo'
|
||||||
|
|
|
||||||
19
storefront/src/pages/cart-page.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<template>
|
||||||
|
<div class="cart">
|
||||||
|
<div class="container">
|
||||||
|
<div class="cart__wrapper">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cart {
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="home">
|
||||||
|
<home-hero />
|
||||||
|
<home-brands />
|
||||||
|
<home-collection />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -8,10 +10,13 @@
|
||||||
import {onMounted} from "vue";
|
import {onMounted} from "vue";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {useUserActivation} from "@/composables/user";
|
import {useUserActivation} from "@/composables/user";
|
||||||
import DepositForm from "@/components/forms/deposit-form.vue";
|
import {useAppStore} from "@/stores/app.js";
|
||||||
import LoginForm from "@/components/forms/login-form.vue";
|
import HomeHero from "@/components/home/home-hero.vue";
|
||||||
|
import HomeCollection from "@/components/home/home-collection.vue";
|
||||||
|
import HomeBrands from "@/components/home/home-brands.vue";
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
const { activateUser } = useUserActivation();
|
const { activateUser } = useUserActivation();
|
||||||
|
|
||||||
|
|
@ -19,9 +24,17 @@ onMounted( async () => {
|
||||||
if (route.name === "activate-user") {
|
if (route.name === "activate-user") {
|
||||||
await activateUser()
|
await activateUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (route.name === "reset-password") {
|
||||||
|
await appStore.setActiveState('new-password')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.home {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 125px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import {computed, onMounted} from "vue";
|
import {computed, onMounted} from "vue";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {useProductbySlug} from "@/composables/products/useProductBySlug.js";
|
import {useProductbySlug} from "@/composables/products";
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
|
|
||||||
24
storefront/src/pages/search-page.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<div class="search">
|
||||||
|
<div class="container">
|
||||||
|
<div class="search__wrapper">
|
||||||
|
<h1>{{ query }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const query = computed(() => route.query.q)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search {
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -16,8 +16,6 @@ const { products, pageInfo, loading, getProducts } = useProducts();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getProducts({})
|
await getProducts({})
|
||||||
console.log('products:', products)
|
|
||||||
console.log('pageInfo:', pageInfo)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
19
storefront/src/pages/wishlist-page.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<template>
|
||||||
|
<div class="wishlist">
|
||||||
|
<div class="container">
|
||||||
|
<div class="wishlist__wrapper">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.wishlist {
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -6,12 +6,15 @@ import NewPasswordForm from "@/components/forms/new-password-form.vue";
|
||||||
import BlogPage from "@/pages/blog-page.vue";
|
import BlogPage from "@/pages/blog-page.vue";
|
||||||
import PostPage from "@/pages/post-page.vue";
|
import PostPage from "@/pages/post-page.vue";
|
||||||
import ProfilePage from "@/pages/profile-page.vue";
|
import ProfilePage from "@/pages/profile-page.vue";
|
||||||
import {useAuthStore} from "@/stores/auth.js";
|
import {useUserStore} from "@/stores/user.js";
|
||||||
import RegisterForm from "@/components/forms/register-form.vue";
|
import RegisterForm from "@/components/forms/register-form.vue";
|
||||||
import LoginForm from "@/components/forms/login-form.vue";
|
import LoginForm from "@/components/forms/login-form.vue";
|
||||||
import ResetPasswordForm from "@/components/forms/reset-password-form.vue";
|
import ResetPasswordForm from "@/components/forms/reset-password-form.vue";
|
||||||
import StorePage from "@/pages/store-page.vue";
|
import StorePage from "@/pages/store-page.vue";
|
||||||
import ProductPage from "@/pages/product-page.vue";
|
import ProductPage from "@/pages/product-page.vue";
|
||||||
|
import SearchPage from "@/pages/search-page.vue";
|
||||||
|
import CartPage from "@/pages/cart-page.vue";
|
||||||
|
import WishlistPage from "@/pages/wishlist-page.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
|
@ -35,41 +38,6 @@ const routes = [
|
||||||
title: 'Home'
|
title: 'Home'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'reset-password',
|
|
||||||
name: 'reset-password',
|
|
||||||
component: NewPasswordForm,
|
|
||||||
meta: {
|
|
||||||
title: 'New Password'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'register',
|
|
||||||
name: 'register',
|
|
||||||
component: RegisterForm,
|
|
||||||
meta: {
|
|
||||||
title: 'Register',
|
|
||||||
requiresGuest: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'login',
|
|
||||||
name: 'login',
|
|
||||||
component: LoginForm,
|
|
||||||
meta: {
|
|
||||||
title: 'Login',
|
|
||||||
requiresGuest: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'forgot-password',
|
|
||||||
name: 'forgot-password',
|
|
||||||
component: ResetPasswordForm,
|
|
||||||
meta: {
|
|
||||||
title: 'Forgot Password',
|
|
||||||
requiresGuest: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'blog',
|
path: 'blog',
|
||||||
name: 'blog',
|
name: 'blog',
|
||||||
|
|
@ -102,6 +70,30 @@ const routes = [
|
||||||
title: 'Product'
|
title: 'Product'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'search',
|
||||||
|
name: 'search',
|
||||||
|
component: SearchPage,
|
||||||
|
meta: {
|
||||||
|
title: 'Search'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'cart',
|
||||||
|
name: 'cart',
|
||||||
|
component: CartPage,
|
||||||
|
meta: {
|
||||||
|
title: 'Cart'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'wishlist',
|
||||||
|
name: 'wishlist',
|
||||||
|
component: WishlistPage,
|
||||||
|
meta: {
|
||||||
|
title: 'Wishlist'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'profile',
|
path: 'profile',
|
||||||
name: 'profile',
|
name: 'profile',
|
||||||
|
|
@ -129,8 +121,8 @@ const router = createRouter({
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
const authStore = useAuthStore();
|
const userStore = useUserStore();
|
||||||
const isAuthenticated = authStore.accessToken
|
const isAuthenticated = userStore.accessToken
|
||||||
|
|
||||||
document.title = to.meta.title ? `${APP_NAME} | ` + to.meta?.title : APP_NAME
|
document.title = to.meta.title ? `${APP_NAME} | ` + to.meta?.title : APP_NAME
|
||||||
|
|
||||||
|
|
|
||||||
25
storefront/src/stores/app.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('app', () => {
|
||||||
|
const activeState = ref(null);
|
||||||
|
|
||||||
|
const setActiveState = (state) => {
|
||||||
|
activeState.value = state;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSignUp = computed(() => activeState.value === "signUp");
|
||||||
|
const isSignIn = computed(() => activeState.value === "signIn");
|
||||||
|
const isForgot = computed(() => activeState.value === "reset-password");
|
||||||
|
const isReset = computed(() => activeState.value === "new-password");
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeState,
|
||||||
|
setActiveState,
|
||||||
|
|
||||||
|
isSignUp,
|
||||||
|
isSignIn,
|
||||||
|
isForgot,
|
||||||
|
isReset
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import {defineStore} from "pinia";
|
|
||||||
import {ref} from "vue";
|
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
|
||||||
const user = ref(null);
|
|
||||||
const accessToken = ref(null);
|
|
||||||
|
|
||||||
const setUser = (payload) => {
|
|
||||||
user.value = payload.user
|
|
||||||
accessToken.value = payload.accessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
return { user, accessToken, setUser }
|
|
||||||
})
|
|
||||||
|
|
@ -7,8 +7,15 @@ export const useLanguageStore = defineStore('language', () => {
|
||||||
languages.value = payload
|
languages.value = payload
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentLocale = ref(null);
|
||||||
|
const setCurrentLocale = (payload) => {
|
||||||
|
currentLocale.value = payload
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
languages,
|
languages,
|
||||||
setLanguages
|
setLanguages,
|
||||||
|
currentLocale,
|
||||||
|
setCurrentLocale
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
12
storefront/src/stores/user.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import {defineStore} from "pinia";
|
||||||
|
import {ref} from "vue";
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const user = ref(null);
|
||||||
|
|
||||||
|
const setUser = (payload) => {
|
||||||
|
user.value = payload.user
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user, setUser }
|
||||||
|
})
|
||||||