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.
This commit is contained in:
Alexandr SaVBaD Waltz 2025-05-31 17:43:33 +03:00
parent 9220de5e63
commit 2d363e1740
100 changed files with 2192 additions and 298 deletions

View file

@ -15,9 +15,12 @@
"graphql": "^16.11.0",
"graphql-tag": "^2.12.6",
"pinia": "^3.0.1",
"primeicons": "^7.0.0",
"swiper": "^11.2.8",
"vue": "^3.5.13",
"vue-i18n": "^11.1.4",
"vue-router": "^4.5.0"
"vue-router": "^4.5.0",
"vue3-marquee-slider": "^1.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
@ -3344,6 +3347,12 @@
"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": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -3618,6 +3627,25 @@
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
@ -4050,6 +4078,18 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -16,9 +16,12 @@
"graphql": "^16.11.0",
"graphql-tag": "^2.12.6",
"pinia": "^3.0.1",
"primeicons": "^7.0.0",
"swiper": "^11.2.8",
"vue": "^3.5.13",
"vue-i18n": "^11.1.4",
"vue-router": "^4.5.0"
"vue-router": "^4.5.0",
"vue3-marquee-slider": "^1.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View file

@ -1,11 +1,21 @@
<script setup>
import { RouterView } from 'vue-router'
import {onMounted} from "vue";
import {computed, onMounted} from "vue";
import {useRefresh} from "@/composables/auth";
import {useCompanyInfo} from "@/composables/company";
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 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 { getCompanyInfo } = useCompanyInfo();
@ -25,6 +35,14 @@ onMounted(async () => {
<template>
<main class="main" id="top">
<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 }">
<Transition name="opacity" mode="out-in">
<component :is="Component" />
@ -35,5 +53,13 @@ onMounted(async () => {
</template>
<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>

View file

@ -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 {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 { useAuthStore } from "@/stores/auth.js";
const httpLink = createHttpLink({
uri: 'https://api.' + import.meta.env.EVIBES_BASE_DOMAIN + '/graphql/',
@ -13,10 +12,8 @@ const userLocale = computed(() => {
});
export const createApolloClient = () => {
const authStore = useAuthStore()
const accessToken = computed(() => {
return authStore.accessToken
return localStorage.getItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY)
})
const authLink = setContext((_, { headers }) => {

View 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

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -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;
}

View file

@ -1,5 +1,9 @@
$font_default: '', sans-serif;
$font_default: 'Source Code Pro', sans-serif;
$white: #ffffff;
$black: #000000;
$accent: #7965d1;
$accentLight: #a69cdc;
$accentDisabled: #826fa2;
$error: #f13838;
$default_border_radius: 4px;

View 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>

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View 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>

View file

@ -30,23 +30,23 @@ import {computed, ref} from "vue";
import UiButton from "@/components/ui/ui-button.vue";
import {useI18n} from "vue-i18n";
import {useDeposit} from "@/composables/user/useDeposit.js";
import {useCompanyStore} from "@/stores/company.js";
const {t} = useI18n()
const companyStore = useCompanyStore()
const paymentMin = computed(() => companyStore.companyInfo?.paymentGatewayMinimum)
const paymentMax = computed(() => companyStore.companyInfo?.paymentGatewayMaximum)
const amount = ref('')
const isFormValid = computed(() => {
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();
async function handleDeposit() {

View file

@ -1,5 +1,6 @@
<template>
<form @submit.prevent="handleLogin()" class="form">
<h2 class="form__title">{{ t('forms.login.title') }}</h2>
<ui-input
:type="'email'"
:placeholder="t('fields.email')"
@ -17,13 +18,26 @@
>
{{ t('checkboxes.remember') }}
</ui-checkbox>
<ui-link
@click="appStore.setActiveState('reset-password')"
>
{{ t('forms.login.forgot') }}
</ui-link>
<ui-button
class="form__button"
:isDisabled="!isFormValid"
:isLoading="loading"
>
{{ t('buttons.signIn') }}
{{ t('buttons.login') }}
</ui-button>
<p class="form__register">
{{ t('forms.login.register') }}
<ui-link
@click="appStore.setActiveState('register')"
>
{{ t('forms.register.title') }}
</ui-link>
</p>
</form>
</template>
@ -35,8 +49,11 @@ import UiInput from "@/components/ui/ui-input.vue";
import UiButton from "@/components/ui/ui-button.vue";
import UiCheckbox from "@/components/ui/ui-checkbox.vue";
import {useLogin} from "@/composables/auth";
import UiLink from "@/components/ui/ui-link.vue";
import {useAppStore} from "@/stores/app.js";
const {t} = useI18n()
const appStore = useAppStore()
const email = ref('')
const password = ref('')
@ -61,5 +78,18 @@ async function handleLogin() {
display: flex;
flex-direction: column;
gap: 20px;
&__title {
font-size: 36px;
color: $accent;
}
&__register {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
}
}
</style>

View file

@ -1,5 +1,6 @@
<template>
<form @submit.prevent="handleReset()" class="form">
<h2 class="form__title">{{ t('forms.newPassword.title') }}</h2>
<ui-input
:type="'password'"
:placeholder="t('fields.newPassword')"
@ -62,5 +63,10 @@ async function handleReset() {
display: flex;
flex-direction: column;
gap: 20px;
&__title {
font-size: 36px;
color: $accent;
}
}
</style>

View file

@ -1,29 +1,34 @@
<template>
<form @submit.prevent="handleRegister()" class="form">
<ui-input
:type="'text'"
:placeholder="t('fields.firstName')"
:rules="[required]"
v-model="firstName"
/>
<ui-input
:type="'text'"
:placeholder="t('fields.lastName')"
:rules="[required]"
v-model="lastName"
/>
<ui-input
:type="'text'"
:placeholder="t('fields.phoneNumber')"
:rules="[required]"
v-model="phoneNumber"
/>
<ui-input
:type="'email'"
:placeholder="t('fields.email')"
:rules="[isEmail]"
v-model="email"
/>
<h2 class="form__title">{{ t('forms.register.title') }}</h2>
<div class="form__box">
<ui-input
:type="'text'"
:placeholder="t('fields.firstName')"
:rules="[required]"
v-model="firstName"
/>
<ui-input
:type="'text'"
:placeholder="t('fields.lastName')"
:rules="[required]"
v-model="lastName"
/>
</div>
<div class="form__box">
<ui-input
:type="'text'"
:placeholder="t('fields.phoneNumber')"
:rules="[required]"
v-model="phoneNumber"
/>
<ui-input
:type="'email'"
:placeholder="t('fields.email')"
:rules="[isEmail]"
v-model="email"
/>
</div>
<ui-input
:type="'password'"
:placeholder="t('fields.password')"
@ -41,8 +46,16 @@
:isDisabled="!isFormValid"
:isLoading="loading"
>
{{ t('buttons.signUp') }}
{{ t('buttons.register') }}
</ui-button>
<p class="form__login">
{{ t('forms.register.login') }}
<ui-link
@click="appStore.setActiveState('login')"
>
{{ t('forms.login.title') }}
</ui-link>
</p>
</form>
</template>
@ -53,8 +66,11 @@ import {computed, ref} from "vue";
import UiInput from "@/components/ui/ui-input.vue";
import UiButton from "@/components/ui/ui-button.vue";
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 appStore = useAppStore()
const firstName = ref('')
const lastName = ref('')
@ -98,5 +114,24 @@ async function handleRegister() {
display: flex;
flex-direction: column;
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>

View file

@ -1,5 +1,6 @@
<template>
<form @submit.prevent="handleReset()" class="form">
<h2 class="form__title">{{ t('forms.reset.title') }}</h2>
<ui-input
:type="'email'"
:placeholder="t('fields.email')"
@ -46,5 +47,10 @@ async function handleReset() {
display: flex;
flex-direction: column;
gap: 20px;
&__title {
font-size: 36px;
color: $accent;
}
}
</style>

View file

@ -51,16 +51,16 @@ import {isEmail, isPasswordValid, required} from "@/core/rules/textFieldRules.js
import {computed, ref, watchEffect} from "vue";
import UiInput from "@/components/ui/ui-input.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";
const {t} = useI18n()
const authStore = useAuthStore()
const userStore = useUserStore()
const userFirstName = computed(() => authStore.user?.firstName)
const userLastName = computed(() => authStore.user?.lastName)
const userEmail = computed(() => authStore.user?.email)
const userPhoneNumber = computed(() => authStore.user?.phoneNumber)
const userFirstName = computed(() => userStore.user?.firstName)
const userLastName = computed(() => userStore.user?.lastName)
const userEmail = computed(() => userStore.user?.email)
const userPhoneNumber = computed(() => userStore.user?.phoneNumber)
const firstName = ref('')
const lastName = ref('')

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -26,32 +26,36 @@ const props = defineProps({
cursor: pointer;
flex-shrink: 0;
transition: 0.2s;
border: 1px solid $black;
background-color: $white;
padding-block: 5px;
border: 1px solid $accent;
background-color: $accent;
border-radius: $default_border_radius;
padding-block: 7px;
display: grid;
place-items: center;
z-index: 1;
color: $black;
color: $white;
text-align: center;
font-size: 14px;
font-weight: 700;
&:hover, &.active {
background-color: $black;
color: $white;
@include hover {
background-color: $accentLight;
}
&.active {
background-color: $accentLight;
}
&:disabled {
cursor: not-allowed;
background-color: rgba($black, 0.5);
color: $black;
background-color: $accentDisabled;
color: $white;
}
&:disabled:hover, &.active {
background-color: rgba($black, 0.5);
color: $black;
background-color: $accentDisabled;
color: $white;
}
&__loader {

View file

@ -1,14 +1,15 @@
<template>
<div>
<div class="checkbox">
<input
:id="'checkbox' + id"
class="checkbox"
:id="id ? `checkbox + id` : 'checkbox'"
class="checkbox__input"
type="checkbox"
:value="modelValue"
@input="onInput"
: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 />
</label>
</div>
@ -24,44 +25,56 @@ const props = defineProps({
const onInput = (event) => {
$emit('update:modelValue', event.target.checked);
};
const toggleCheckbox = () => {
$emit('update:modelValue', !props.modelValue);
};
</script>
<style lang="scss" scoped>
.checkbox {
display: none;
opacity: 0;
display: flex;
align-items: center;
gap: 5px;
&__input {
display: none;
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 {
cursor: pointer;
color: #2B2B2B;
font-size: 12px;
font-weight: 400;
font-weight: 500;
line-height: 16px;
letter-spacing: 0.12px;
}
}
.checkbox + .checkbox__label::before {
content: '';
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;
.checkbox__input:checked + .checkbox__block::after {
opacity: 1;
}
</style>

View 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>

View file

@ -12,10 +12,10 @@
<button
@click.prevent="setPasswordVisible"
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">
<img v-else src="@icons/eyeOpened.svg" alt="eye" loading="lazy">
<i v-if="isPasswordVisible === 'password'" class="pi pi-eye-slash"></i>
<i v-else class="pi pi-eye"></i>
</button>
</div>
<p v-if="!validate" class="block__error">{{ errorMessage }}</p>
@ -99,7 +99,9 @@ const onInput = (e) => {
&__input {
width: 100%;
padding: 6px 12px;
border: 1px solid $black;
border: 1px solid #e0e0e0;
//border: 1px solid #b2b2b2;
border-radius: $default_border_radius;
background-color: $white;
color: #1f1f1f;
@ -121,6 +123,8 @@ const onInput = (e) => {
background-color: transparent;
display: grid;
place-items: center;
font-size: 18px;
color: #838383;
}
&__error {

View 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>

View 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>

View file

@ -3,13 +3,12 @@ import {LOGIN} from "@/graphql/mutations/auth.js";
import {ref} from "vue";
import {ElNotification} from "element-plus";
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 {
DEFAULT_LOCALE,
DEFAULT_LOCALE, LOCALE_STORAGE_ACCESS_TOKEN_KEY,
LOCALE_STORAGE_LOCALE_KEY,
LOCALE_STORAGE_REFRESH_KEY,
LOCALE_STORAGE_STAY_LOGIN_KEY
LOCALE_STORAGE_REFRESH_TOKEN_KEY,
} from "@/config/index.js";
import {useRoute, useRouter} from "vue-router";
import {usePendingOrder} from "@/composables/orders";
@ -18,7 +17,7 @@ import {useWishlist} from "@/composables/wishlist";
export function useLogin() {
const router = useRouter();
const route = useRoute();
const authStore = useAuthStore()
const userStore = useUserStore()
const {t} = useI18n();
const { mutate: loginMutation } = useMutation(LOGIN);
@ -42,16 +41,14 @@ export function useLogin() {
});
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) {
authStore.setUser({
user: response.data.obtainJwtToken.user,
accessToken: response.data.obtainJwtToken.accessToken
userStore.setUser({
user: response.data.obtainJwtToken.user
});
localStorage.setItem(LOCALE_STORAGE_REFRESH_KEY, response.data.obtainJwtToken.refreshToken)
localStorage.setItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY, response.data.obtainJwtToken.accessToken)
ElNotification({
message: t('popup.success.login'),

View file

@ -1,24 +1,23 @@
import {useAuthStore} from "@/stores/auth.js";
import {useUserStore} from "@/stores/user.js";
import {
DEFAULT_LOCALE,
LOCALE_STORAGE_LOCALE_KEY,
LOCALE_STORAGE_REFRESH_KEY,
LOCALE_STORAGE_STAY_LOGIN_KEY
DEFAULT_LOCALE, LOCALE_STORAGE_ACCESS_TOKEN_KEY,
LOCALE_STORAGE_LOCALE_KEY,
LOCALE_STORAGE_REFRESH_TOKEN_KEY
} from "@/config/index.js";
import {useRouter} from "vue-router";
export function useLogout() {
const authStore = useAuthStore()
const userStore = useUserStore()
const router = useRouter()
async function logout() {
authStore.setUser({
userStore.setUser({
user: null,
accessToken: null
})
localStorage.removeItem(LOCALE_STORAGE_REFRESH_KEY)
localStorage.removeItem(LOCALE_STORAGE_STAY_LOGIN_KEY)
localStorage.removeItem(LOCALE_STORAGE_REFRESH_TOKEN_KEY)
localStorage.removeItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY)
await router.push({
name: 'home',

View file

@ -3,8 +3,8 @@ import {REFRESH} from "@/graphql/mutations/auth.js";
import {computed, ref} from "vue";
import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {useAuthStore} from "@/stores/auth.js";
import {LOCALE_STORAGE_REFRESH_KEY} from "@/config/index.js";
import {useUserStore} from "@/stores/user.js";
import {LOCALE_STORAGE_REFRESH_TOKEN_KEY} from "@/config/index.js";
import {useRoute, useRouter} from "vue-router";
import translations from "@/core/helpers/translations.js";
import {usePendingOrder} from "@/composables/orders";
@ -13,7 +13,7 @@ import {useWishlist} from "@/composables/wishlist";
export function useRefresh() {
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
const userStore = useUserStore()
const {t} = useI18n();
const { mutate: refreshMutation } = useMutation(REFRESH);
@ -26,7 +26,7 @@ export function useRefresh() {
async function refresh() {
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
@ -36,7 +36,7 @@ export function useRefresh() {
});
if (response.data?.refreshJwtToken) {
authStore.setUser({
userStore.setUser({
user: response.data.refreshJwtToken.user,
accessToken: response.data.refreshJwtToken.accessToken
})
@ -45,7 +45,7 @@ export function useRefresh() {
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 getWishlist();

View file

@ -0,0 +1,2 @@
export * from './useBrands'
export * from './useBrandByUuid'

View 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
};
}

View 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
};
}

View file

@ -2,13 +2,13 @@ import { useLazyQuery } from "@vue/apollo-composable";
import {computed} from "vue";
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 category = computed(() => result.value?.categories.edges[0].node ?? []);
if (error.value) {
console.error("usePostbySlug error:", error.value);
console.error("useCategorybySlug error:", error.value);
}
const getCategory = (slug) => {

View file

@ -2,18 +2,19 @@ import {useMutation} from "@vue/apollo-composable";
import {computed, ref} from "vue";
import {ElNotification} from "element-plus";
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 {SWITCH_LANGUAGE} from "@/graphql/mutations/languages.js";
import {LOCALE_STORAGE_ACCESS_TOKEN_KEY} from "@/config/index.js";
export function useLanguageSwitch() {
const authStore = useAuthStore()
const userStore = useUserStore()
const {t} = useI18n();
const { mutate: languageSwitchMutation } = useMutation(SWITCH_LANGUAGE);
const accessToken = computed(() => authStore.accessToken)
const userUuid = computed(() => authStore.user?.uuid)
const accessToken = computed(() => localStorage.getItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY))
const userUuid = computed(() => userStore.user?.uuid)
const loading = ref(false);
@ -31,7 +32,7 @@ export function useLanguageSwitch() {
);
if (response.data?.updateUser) {
authStore.setUser({
userStore.setUser({
user: response.data.updateUser.user,
accessToken: accessToken.value
})

View file

@ -2,7 +2,7 @@ import { useLazyQuery } from "@vue/apollo-composable";
import {watchEffect} from "vue";
import {GET_LANGUAGES} from "@/graphql/queries/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() {
const languageStore = useLanguageStore()
@ -22,6 +22,8 @@ export function useLanguages() {
)
)
);
languageStore.setCurrentLocale(languageStore.languages.find((locale) => locale.code === localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY)))
}
});

View file

@ -1 +1,3 @@
export * from './useProducts'
export * from './useProductBySlug'
export * from './useProductTags'

View 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
};
}

View file

@ -1,46 +1,39 @@
import { ref } from 'vue';
import { useQuery } from '@vue/apollo-composable';
import {GET_PRODUCTS} from "@/graphql/queries/products.js";
import { useLazyQuery } from "@vue/apollo-composable";
import { computed, ref } from "vue";
import { GET_PRODUCTS } from "@/graphql/queries/products.js";
export function useProducts() {
const products = ref([]);
const pageInfo = ref([]);
const loading = ref(false);
const variables = ref({
first: 12
});
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 = {}) => {
loading.value = true;
const defaults = {
first: 12
};
const variables = {};
const newVariables = { ...variables.value };
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
variables[key] = value;
newVariables[key] = value;
}
});
Object.entries(defaults).forEach(([key, value]) => {
if (!(key in variables)) {
variables[key] = value;
}
});
variables.value = newVariables;
try {
const { onResult } = useQuery(GET_PRODUCTS, variables);
onResult(result => {
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;
if (result.value) {
await refetch();
} else {
await load();
}
};
@ -48,6 +41,7 @@ export function useProducts() {
products,
pageInfo,
loading,
error,
getProducts
};
}

View file

@ -0,0 +1,2 @@
export * from './useSearch'
export * from './useSearchUi'

View 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
};
}

View 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
};
}

View file

@ -2,25 +2,26 @@ import {useMutation} from "@vue/apollo-composable";
import {computed, ref} from "vue";
import {ElNotification} from "element-plus";
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 {useRoute, useRouter} from "vue-router";
import {useLogout} from "@/composables/auth";
import {UPDATE_USER} from "@/graphql/mutations/user.js";
import {LOCALE_STORAGE_ACCESS_TOKEN_KEY} from "@/config/index.js";
export function useUserUpdating() {
const router = useRouter();
const route = useRoute();
const authStore = useAuthStore()
const userStore = useUserStore()
const {t} = useI18n();
const { mutate: userUpdatingMutation } = useMutation(UPDATE_USER);
const { logout } = useLogout();
const accessToken = computed(() => authStore.accessToken)
const userUuid = computed(() => authStore.user?.uuid)
const userEmail = computed(() => authStore.user?.email)
const accessToken = computed(() => localStorage.getItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY))
const userUuid = computed(() => userStore.user?.uuid)
const userEmail = computed(() => userStore.user?.email)
const loading = ref(false);
@ -81,7 +82,7 @@ export function useUserUpdating() {
type: "success"
});
} else {
authStore.setUser({
userStore.setUser({
user: response.data.updateUser.user,
accessToken: accessToken.value
})

View file

@ -8,10 +8,70 @@ export const APP_NAME_KEY = APP_NAME.toLowerCase()
// LOCALES
export const SUPPORTED_LOCALES = [
export const SUPPORTED_LOCALES = [
{
code: 'en-gb',
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_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`;

View file

@ -0,0 +1,8 @@
import gql from 'graphql-tag'
export const BRAND_FRAGMENT = gql`
fragment Brand on BrandType {
uuid
name
}
`

View file

@ -1,12 +0,0 @@
import gql from 'graphql-tag'
export const COMPANY_FRAGMENT = gql`
fragment Company on ConfigType {
companyAddress
companyName
companyPhoneNumber
emailFrom
emailHostUser
projectName
}
`

View file

@ -1,9 +0,0 @@
import gql from 'graphql-tag'
export const LANGUAGES_FRAGMENT = gql`
fragment Languages on LanguageType {
code
flag
name
}
`

View file

@ -33,5 +33,21 @@ export const PRODUCT_FRAGMENT = gql`
}
}
}
feedbacks {
edges {
node {
uuid
rating
}
}
}
tags {
edges {
node {
tagName
name
}
}
}
}
`

View 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
}
}
}
}
`

View 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}
`

View file

@ -7,6 +7,15 @@ export const GET_CATEGORIES = gql`
edges {
node {
...Category
children {
...Category
children {
...Category
children {
...Category
}
}
}
}
}
}

View file

@ -1,11 +1,16 @@
import gql from 'graphql-tag'
import {COMPANY_FRAGMENT} from "@/graphql/fragments/company.fragment.js";
export const GET_COMPANY_INFO = gql`
query getCompanyInfo {
parameters {
...Company
companyAddress
companyName
companyPhoneNumber
emailFrom
emailHostUser
projectName
paymentGatewayMinimum
paymentGatewayMaximum
}
}
${COMPANY_FRAGMENT}
`

View file

@ -1,11 +1,11 @@
import gql from 'graphql-tag'
import {LANGUAGES_FRAGMENT} from "@/graphql/fragments/languages.fragment.js";
export const GET_LANGUAGES = gql`
query getLanguages {
languages {
...Languages
code
flag
name
}
}
${LANGUAGES_FRAGMENT}
`

View file

@ -51,3 +51,25 @@ export const GET_PRODUCT_BY_SLUG = gql`
}
${PRODUCT_FRAGMENT}
`
export const GET_PRODUCT_TAGS = gql`
query getProductTags {
productTags {
edges {
node {
uuid
name
tagName
productSet {
edges {
node {
...Product
}
}
}
}
}
}
}
${PRODUCT_FRAGMENT}
`

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -1,7 +1,7 @@
{
"buttons": {
"signIn": "Sign In",
"signUp": "Sign Up",
"login": "Login",
"register": "Register",
"addToCart": "Add To Cart",
"send": "Send",
"goEmail": "Take me to my inbox",
@ -23,7 +23,7 @@
"pageNotFound": "Page not found"
},
"fields": {
"search": "Search Cards",
"search": "Search",
"name": "Name",
"firstName": "First name",
"lastName": "Last name",
@ -62,5 +62,47 @@
"payment": "Your purchase is being processed! Please stand by",
"successCheckout": "Order purchase successful!",
"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: "
}
}
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -1,5 +1,6 @@
import '@/assets/styles/global/fonts.scss'
import '@/assets/styles/main.scss'
import 'primeicons/primeicons.css'
import {createApp, h, provide} from 'vue'
import { DefaultApolloClient } from '@vue/apollo-composable'
import { createApolloClient } from './apollo'

View 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>

View file

@ -1,6 +1,8 @@
<template>
<div>
<div class="home">
<home-hero />
<home-brands />
<home-collection />
</div>
</template>
@ -8,10 +10,13 @@
import {onMounted} from "vue";
import {useRoute} from "vue-router";
import {useUserActivation} from "@/composables/user";
import DepositForm from "@/components/forms/deposit-form.vue";
import LoginForm from "@/components/forms/login-form.vue";
import {useAppStore} from "@/stores/app.js";
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 appStore = useAppStore()
const { activateUser } = useUserActivation();
@ -19,9 +24,17 @@ onMounted( async () => {
if (route.name === "activate-user") {
await activateUser()
}
if (route.name === "reset-password") {
await appStore.setActiveState('new-password')
}
})
</script>
<style lang="scss" scoped>
.home {
display: flex;
flex-direction: column;
gap: 125px;
}
</style>

View file

@ -11,7 +11,7 @@
<script setup>
import {computed, onMounted} from "vue";
import {useRoute} from "vue-router";
import {useProductbySlug} from "@/composables/products/useProductBySlug.js";
import {useProductbySlug} from "@/composables/products";
const route = useRoute()

View 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>

View file

@ -16,8 +16,6 @@ const { products, pageInfo, loading, getProducts } = useProducts();
onMounted(async () => {
await getProducts({})
console.log('products:', products)
console.log('pageInfo:', pageInfo)
})
</script>

View 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>

View file

@ -6,12 +6,15 @@ import NewPasswordForm from "@/components/forms/new-password-form.vue";
import BlogPage from "@/pages/blog-page.vue";
import PostPage from "@/pages/post-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 LoginForm from "@/components/forms/login-form.vue";
import ResetPasswordForm from "@/components/forms/reset-password-form.vue";
import StorePage from "@/pages/store-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 = [
{
@ -35,41 +38,6 @@ const routes = [
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',
name: 'blog',
@ -102,6 +70,30 @@ const routes = [
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',
name: 'profile',
@ -129,8 +121,8 @@ const router = createRouter({
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore();
const isAuthenticated = authStore.accessToken
const userStore = useUserStore();
const isAuthenticated = userStore.accessToken
document.title = to.meta.title ? `${APP_NAME} | ` + to.meta?.title : APP_NAME

View 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
};
});

View file

@ -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 }
})

View file

@ -7,8 +7,15 @@ export const useLanguageStore = defineStore('language', () => {
languages.value = payload
};
const currentLocale = ref(null);
const setCurrentLocale = (payload) => {
currentLocale.value = payload
};
return {
languages,
setLanguages
setLanguages,
currentLocale,
setCurrentLocale
}
})

View 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 }
})