Features: 1) Add Apollo Client integration with support for locale-based headers; 2) Add Czech and German locale files; 3) Add Source Code Pro Bold font;
Fixes: None; Extra: None;
This commit is contained in:
parent
dbdf20b1b1
commit
8a8a1605ea
129 changed files with 4891 additions and 0 deletions
41
storefront/src/apollo/index.js
Normal file
41
storefront/src/apollo/index.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import {ApolloClient, ApolloLink, createHttpLink, InMemoryCache} from '@apollo/client/core'
|
||||
import { setContext } from '@apollo/client/link/context'
|
||||
import {DEFAULT_LOCALE, LOCALE_STORAGE_ACCESS_TOKEN_KEY, LOCALE_STORAGE_LOCALE_KEY} from "@/config/index.js";
|
||||
import {computed} from "vue";
|
||||
|
||||
const httpLink = createHttpLink({
|
||||
uri: 'https://api.' + import.meta.env.EVIBES_BASE_DOMAIN + '/graphql/',
|
||||
});
|
||||
|
||||
const userLocale = computed(() => {
|
||||
return localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY)
|
||||
});
|
||||
|
||||
export const createApolloClient = () => {
|
||||
const accessToken = computed(() => {
|
||||
return localStorage.getItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY)
|
||||
})
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
const baseHeaders = {
|
||||
...headers,
|
||||
"Accept-language": userLocale.value ? userLocale.value : DEFAULT_LOCALE,
|
||||
};
|
||||
|
||||
if (accessToken.value) {
|
||||
baseHeaders["X-EVIBES-AUTH"] = `Bearer ${accessToken.value}`;
|
||||
}
|
||||
|
||||
return { headers: baseHeaders };
|
||||
})
|
||||
|
||||
return new ApolloClient({
|
||||
link: authLink.concat(httpLink),
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Binary file not shown.
BIN
storefront/src/assets/fonts/SourceCodePro/SourceCodePro-Bold.ttf
Normal file
BIN
storefront/src/assets/fonts/SourceCodePro/SourceCodePro-Bold.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
storefront/src/assets/images/evibes-big-simple.png
Normal file
BIN
storefront/src/assets/images/evibes-big-simple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
storefront/src/assets/images/evibes-big.png
Normal file
BIN
storefront/src/assets/images/evibes-big.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
BIN
storefront/src/assets/images/homeBg.png
Normal file
BIN
storefront/src/assets/images/homeBg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
42
storefront/src/assets/styles/global/fonts.scss
Normal file
42
storefront/src/assets/styles/global/fonts.scss
Normal 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;
|
||||
}
|
||||
7
storefront/src/assets/styles/global/mixins.scss
Normal file
7
storefront/src/assets/styles/global/mixins.scss
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
@mixin hover {
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
storefront/src/assets/styles/global/variables.scss
Normal file
9
storefront/src/assets/styles/global/variables.scss
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
$font_default: 'Source Code Pro', sans-serif;
|
||||
|
||||
$white: #ffffff;
|
||||
$black: #000000;
|
||||
$accent: #7965d1;
|
||||
$accentLight: #a69cdc;
|
||||
$accentDisabled: #826fa2;
|
||||
$error: #f13838;
|
||||
$default_border_radius: 4px;
|
||||
4
storefront/src/assets/styles/main.scss
Normal file
4
storefront/src/assets/styles/main.scss
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
@use "modules/normalize";
|
||||
@use "modules/transitions";
|
||||
@use "global/mixins";
|
||||
@use "global/variables";
|
||||
49
storefront/src/assets/styles/modules/normalize.scss
vendored
Normal file
49
storefront/src/assets/styles/modules/normalize.scss
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
@use "../global/variables" as *;
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
font-family: $font_default;
|
||||
}
|
||||
|
||||
#app {
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
input, textarea, button {
|
||||
font-family: $font_default;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1500px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 1680px) {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1300px) {
|
||||
.container {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
28
storefront/src/assets/styles/modules/transitions.scss
Normal file
28
storefront/src/assets/styles/modules/transitions.scss
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
.opacity-enter-active,
|
||||
.opacity-leave-active {
|
||||
transition: 0.3s ease all;
|
||||
}
|
||||
.opacity-enter-from,
|
||||
.opacity-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fromTop-enter-active,
|
||||
.fromTop-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.fromTop-enter-from,
|
||||
.fromTop-leave-to {
|
||||
transform: translateY(-3rem);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fromLeft-enter-active,
|
||||
.fromLeft-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.fromLeft-enter-from,
|
||||
.fromLeft-leave-to {
|
||||
transform: translateX(-3rem);
|
||||
opacity: 0;
|
||||
}
|
||||
65
storefront/src/components/base/base-auth.vue
Normal file
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>
|
||||
19
storefront/src/components/base/base-footer.vue
Normal file
19
storefront/src/components/base/base-footer.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer_wrapper">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.footer {
|
||||
|
||||
}
|
||||
</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
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
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
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>
|
||||
88
storefront/src/components/forms/contact-form.vue
Normal file
88
storefront/src/components/forms/contact-form.vue
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<form @submit.prevent="handleContactUs()" class="form">
|
||||
<ui-input
|
||||
:type="'text'"
|
||||
:placeholder="t('fields.name')"
|
||||
:rules="[required]"
|
||||
v-model="name"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'email'"
|
||||
:placeholder="t('fields.email')"
|
||||
:rules="[required]"
|
||||
v-model="email"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'text'"
|
||||
:placeholder="t('fields.phoneNumber')"
|
||||
:rules="[required]"
|
||||
v-model="phoneNumber"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'text'"
|
||||
:placeholder="t('fields.subject')"
|
||||
:rules="[required]"
|
||||
v-model="subject"
|
||||
/>
|
||||
<ui-textarea
|
||||
:placeholder="t('fields.message')"
|
||||
:rules="[required]"
|
||||
v-model="message"
|
||||
/>
|
||||
<ui-button
|
||||
class="form__button"
|
||||
:isDisabled="!isFormValid"
|
||||
:isLoading="loading"
|
||||
>
|
||||
{{ t('buttons.send') }}
|
||||
</ui-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {required} from "@/core/rules/textFieldRules.js";
|
||||
import {computed, ref} from "vue";
|
||||
import UiInput from "@/components/ui/ui-input.vue";
|
||||
import UiButton from "@/components/ui/ui-button.vue";
|
||||
import UiTextarea from "@/components/ui/ui-textarea.vue";
|
||||
import {useContactUs} from "@/composables/contact/index.js";
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const name = ref('')
|
||||
const email = ref('')
|
||||
const phoneNumber = ref('')
|
||||
const subject = ref('')
|
||||
const message = ref('')
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
required(name.value) === true &&
|
||||
required(email.value) === true &&
|
||||
required(phoneNumber.value) === true &&
|
||||
required(subject.value) === true &&
|
||||
required(message.value) === true
|
||||
)
|
||||
})
|
||||
|
||||
const { contactUs, loading } = useContactUs();
|
||||
|
||||
async function handleContactUs() {
|
||||
await contactUs(
|
||||
name.value,
|
||||
email.value,
|
||||
phoneNumber.value,
|
||||
subject.value,
|
||||
message.value,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
</style>
|
||||
65
storefront/src/components/forms/deposit-form.vue
Normal file
65
storefront/src/components/forms/deposit-form.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<form @submit.prevent="handleDeposit()" class="form">
|
||||
<div class="form__box">
|
||||
<ui-input
|
||||
:type="'text'"
|
||||
:placeholder="''"
|
||||
v-model="amount"
|
||||
:numberOnly="true"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'text'"
|
||||
:placeholder="''"
|
||||
v-model="amount"
|
||||
:numberOnly="true"
|
||||
/>
|
||||
</div>
|
||||
<ui-button
|
||||
class="form__button"
|
||||
:isDisabled="!isFormValid"
|
||||
:isLoading="loading"
|
||||
>
|
||||
{{ t('buttons.topUp') }}
|
||||
</ui-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import UiInput from "@/components/ui/ui-input.vue";
|
||||
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 >= paymentMin.value &&
|
||||
amount.value <= paymentMax.value
|
||||
)
|
||||
})
|
||||
|
||||
const { deposit, loading } = useDeposit();
|
||||
|
||||
async function handleDeposit() {
|
||||
await deposit(amount.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form {
|
||||
&__box {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
95
storefront/src/components/forms/login-form.vue
Normal file
95
storefront/src/components/forms/login-form.vue
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<form @submit.prevent="handleLogin()" class="form">
|
||||
<h2 class="form__title">{{ t('forms.login.title') }}</h2>
|
||||
<ui-input
|
||||
:type="'email'"
|
||||
:placeholder="t('fields.email')"
|
||||
:rules="[isEmail]"
|
||||
v-model="email"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'password'"
|
||||
:placeholder="t('fields.password')"
|
||||
:rules="[required]"
|
||||
v-model="password"
|
||||
/>
|
||||
<ui-checkbox
|
||||
v-model="isStayLogin"
|
||||
>
|
||||
{{ 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.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>
|
||||
|
||||
<script setup>
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {isEmail, required} from "@/core/rules/textFieldRules.js";
|
||||
import {computed, ref} from "vue";
|
||||
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('')
|
||||
const isStayLogin = ref(false)
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
isEmail(email.value) === true &&
|
||||
required(password.value) === true
|
||||
)
|
||||
})
|
||||
|
||||
const { login, loading } = useLogin();
|
||||
|
||||
async function handleLogin() {
|
||||
await login(email.value, password.value, isStayLogin.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form {
|
||||
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>
|
||||
72
storefront/src/components/forms/new-password-form.vue
Normal file
72
storefront/src/components/forms/new-password-form.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<form @submit.prevent="handleReset()" class="form">
|
||||
<h2 class="form__title">{{ t('forms.newPassword.title') }}</h2>
|
||||
<ui-input
|
||||
:type="'password'"
|
||||
:placeholder="t('fields.newPassword')"
|
||||
:rules="[isPasswordValid]"
|
||||
v-model="password"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'password'"
|
||||
:placeholder="t('fields.confirmNewPassword')"
|
||||
:rules="[compareStrings]"
|
||||
v-model="confirmPassword"
|
||||
/>
|
||||
<ui-button
|
||||
class="form__button"
|
||||
:isDisabled="!isFormValid"
|
||||
:isLoading="loading"
|
||||
>
|
||||
{{ t('buttons.save') }}
|
||||
</ui-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {isPasswordValid,} from "@/core/rules/textFieldRules.js";
|
||||
import {computed, ref} from "vue";
|
||||
import UiInput from "@/components/ui/ui-input.vue";
|
||||
import UiButton from "@/components/ui/ui-button.vue";
|
||||
import {useNewPassword} from "@/composables/auth";
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
const compareStrings = (v) => {
|
||||
if (v === password.value) return true
|
||||
return t('errors.compare')
|
||||
}
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
isPasswordValid(password.value) === true &&
|
||||
compareStrings(confirmPassword.value) === true
|
||||
)
|
||||
})
|
||||
|
||||
const { newPassword, loading } = useNewPassword();
|
||||
|
||||
async function handleReset() {
|
||||
await newPassword(
|
||||
password.value,
|
||||
confirmPassword.value,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
137
storefront/src/components/forms/register-form.vue
Normal file
137
storefront/src/components/forms/register-form.vue
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<template>
|
||||
<form @submit.prevent="handleRegister()" class="form">
|
||||
<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')"
|
||||
:rules="[isPasswordValid]"
|
||||
v-model="password"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'password'"
|
||||
:placeholder="t('fields.confirmPassword')"
|
||||
:rules="[compareStrings]"
|
||||
v-model="confirmPassword"
|
||||
/>
|
||||
<ui-button
|
||||
class="form__button"
|
||||
:isDisabled="!isFormValid"
|
||||
:isLoading="loading"
|
||||
>
|
||||
{{ 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>
|
||||
|
||||
<script setup>
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {isEmail, isPasswordValid, required} from "@/core/rules/textFieldRules.js";
|
||||
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('')
|
||||
const phoneNumber = ref('')
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
const compareStrings = (v) => {
|
||||
if (v === password.value) return true
|
||||
return t('errors.compare')
|
||||
}
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
required(firstName.value) === true &&
|
||||
required(lastName.value) === true &&
|
||||
required(phoneNumber.value) === true &&
|
||||
isEmail(email.value) === true &&
|
||||
isPasswordValid(password.value) === true &&
|
||||
compareStrings(confirmPassword.value) === true
|
||||
)
|
||||
})
|
||||
|
||||
const { register, loading } = useRegister();
|
||||
|
||||
async function handleRegister() {
|
||||
await register(
|
||||
firstName.value,
|
||||
lastName.value,
|
||||
phoneNumber.value,
|
||||
email.value,
|
||||
password.value,
|
||||
confirmPassword.value
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form {
|
||||
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>
|
||||
56
storefront/src/components/forms/reset-password-form.vue
Normal file
56
storefront/src/components/forms/reset-password-form.vue
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<form @submit.prevent="handleReset()" class="form">
|
||||
<h2 class="form__title">{{ t('forms.reset.title') }}</h2>
|
||||
<ui-input
|
||||
:type="'email'"
|
||||
:placeholder="t('fields.email')"
|
||||
:rules="[isEmail]"
|
||||
v-model="email"
|
||||
/>
|
||||
<ui-button
|
||||
class="form__button"
|
||||
:isDisabled="!isFormValid"
|
||||
:isLoading="loading"
|
||||
>
|
||||
{{ t('buttons.sendLink') }}
|
||||
</ui-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {isEmail} from "@/core/rules/textFieldRules.js";
|
||||
import {computed, ref} from "vue";
|
||||
import UiInput from "@/components/ui/ui-input.vue";
|
||||
import UiButton from "@/components/ui/ui-button.vue";
|
||||
import {usePasswordReset} from "@/composables/auth";
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const email = ref('')
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
isEmail(email.value) === true
|
||||
)
|
||||
})
|
||||
|
||||
const { resetPassword, loading } = usePasswordReset();
|
||||
|
||||
async function handleReset() {
|
||||
await resetPassword(email.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
104
storefront/src/components/forms/update-form.vue
Normal file
104
storefront/src/components/forms/update-form.vue
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<form class="form" @submit.prevent="handleUpdate()">
|
||||
<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="'email'"
|
||||
:placeholder="t('fields.email')"
|
||||
:rules="[isEmail]"
|
||||
v-model="email"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'text'"
|
||||
:placeholder="t('fields.phoneNumber')"
|
||||
:rules="[required]"
|
||||
v-model="phoneNumber"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'password'"
|
||||
:placeholder="t('fields.newPassword')"
|
||||
:rules="[isPasswordValid]"
|
||||
v-model="password"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'password'"
|
||||
:placeholder="t('fields.confirmNewPassword')"
|
||||
:rules="[compareStrings]"
|
||||
v-model="confirmPassword"
|
||||
/>
|
||||
<ui-button
|
||||
class="form__button"
|
||||
:isLoading="loading"
|
||||
>
|
||||
{{ t('buttons.save') }}
|
||||
</ui-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useI18n} from "vue-i18n";
|
||||
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 {useUserStore} from "@/stores/user.js";
|
||||
import {useUserUpdating} from "@/composables/user";
|
||||
|
||||
const {t} = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
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('')
|
||||
const email = ref('')
|
||||
const phoneNumber = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
const compareStrings = (v) => {
|
||||
if (v === password.value) return true
|
||||
return t('errors.compare')
|
||||
}
|
||||
|
||||
const { updateUser, loading } = useUserUpdating();
|
||||
|
||||
watchEffect(() => {
|
||||
firstName.value = userFirstName.value || ''
|
||||
lastName.value = userLastName.value || ''
|
||||
email.value = userEmail.value || ''
|
||||
phoneNumber.value = userPhoneNumber.value || ''
|
||||
})
|
||||
|
||||
async function handleUpdate() {
|
||||
await updateUser(
|
||||
firstName.value,
|
||||
lastName.value,
|
||||
email.value,
|
||||
phoneNumber.value,
|
||||
password.value,
|
||||
confirmPassword.value,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
</style>
|
||||
48
storefront/src/components/home/home-brands.vue
Normal file
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
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
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
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
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>
|
||||
65
storefront/src/components/ui/ui-button.vue
Normal file
65
storefront/src/components/ui/ui-button.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<button
|
||||
class="button"
|
||||
:disabled="isDisabled"
|
||||
:class="[{active: isLoading}]"
|
||||
type="submit"
|
||||
>
|
||||
<ui-loader class="button__loader" v-if="isLoading" />
|
||||
<slot v-else />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import UiLoader from "@/components/ui/ui-loader.vue";
|
||||
|
||||
const props = defineProps({
|
||||
isDisabled: Boolean,
|
||||
isLoading: Boolean
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: 0.2s;
|
||||
border: 1px solid $accent;
|
||||
background-color: $accent;
|
||||
border-radius: $default_border_radius;
|
||||
padding-block: 7px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
z-index: 1;
|
||||
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
|
||||
@include hover {
|
||||
background-color: $accentLight;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $accentLight;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: $accentDisabled;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:disabled:hover, &.active {
|
||||
background-color: $accentDisabled;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&__loader {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
80
storefront/src/components/ui/ui-checkbox.vue
Normal file
80
storefront/src/components/ui/ui-checkbox.vue
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div class="checkbox">
|
||||
<input
|
||||
:id="id ? `checkbox + id` : 'checkbox'"
|
||||
class="checkbox__input"
|
||||
type="checkbox"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
:checked="modelValue"
|
||||
>
|
||||
<span class="checkbox__block" @click="toggleCheckbox"></span>
|
||||
<label :for="id ? `checkbox + id` : 'checkbox'" class="checkbox__label">
|
||||
<slot />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const $emit = defineEmits()
|
||||
const props = defineProps({
|
||||
id: [Number, String],
|
||||
modelValue: Boolean
|
||||
})
|
||||
|
||||
const onInput = (event) => {
|
||||
$emit('update:modelValue', event.target.checked);
|
||||
};
|
||||
|
||||
const toggleCheckbox = () => {
|
||||
$emit('update:modelValue', !props.modelValue);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.checkbox {
|
||||
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: 500;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox__input:checked + .checkbox__block::after {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
27
storefront/src/components/ui/ui-counter.vue
Normal file
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>
|
||||
148
storefront/src/components/ui/ui-input.vue
Normal file
148
storefront/src/components/ui/ui-input.vue
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<template>
|
||||
<div class="block">
|
||||
<div class="block__inner">
|
||||
<input
|
||||
:placeholder="placeholder"
|
||||
:type="isPasswordVisible"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
@keydown="numberOnly ? onlyNumbersKeydown($event) : null"
|
||||
class="block__input"
|
||||
>
|
||||
<button
|
||||
@click.prevent="setPasswordVisible"
|
||||
class="block__eyes"
|
||||
v-if="type === 'password' && modelValue"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
|
||||
const $emit = defineEmits()
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
placeholder: String,
|
||||
isError: Boolean,
|
||||
error: String,
|
||||
modelValue: [String, Number],
|
||||
rules: Array,
|
||||
numberOnly: Boolean
|
||||
})
|
||||
|
||||
const isPasswordVisible = ref(props.type)
|
||||
const setPasswordVisible = () => {
|
||||
if (isPasswordVisible.value === 'password') {
|
||||
isPasswordVisible.value = 'text'
|
||||
return
|
||||
}
|
||||
isPasswordVisible.value = 'password'
|
||||
}
|
||||
|
||||
const onlyNumbersKeydown = (event) => {
|
||||
if (!/^\d$/.test(event.key) &&
|
||||
!['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'Tab', 'Home', 'End'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
const validate = ref(true)
|
||||
const errorMessage = ref('')
|
||||
const onInput = (e) => {
|
||||
let value = e.target.value;
|
||||
|
||||
if (props.numberOnly) {
|
||||
const newValue = value.replace(/\D/g, '');
|
||||
if (newValue !== value) {
|
||||
e.target.value = newValue;
|
||||
value = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
let result = true
|
||||
|
||||
props.rules?.forEach((rule) => {
|
||||
result = rule((e.target).value)
|
||||
|
||||
if (result !== true) {
|
||||
errorMessage.value = String(result)
|
||||
result = false
|
||||
}
|
||||
})
|
||||
|
||||
validate.value = result
|
||||
|
||||
return $emit('update:modelValue', (e.target).value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.block {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
|
||||
&__inner {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
//border: 1px solid #b2b2b2;
|
||||
border-radius: $default_border_radius;
|
||||
background-color: $white;
|
||||
|
||||
color: #1f1f1f;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
&::placeholder {
|
||||
color: #2B2B2B;
|
||||
}
|
||||
}
|
||||
|
||||
&__eyes {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background-color: transparent;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 18px;
|
||||
color: #838383;
|
||||
}
|
||||
|
||||
&__error {
|
||||
color: $error;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
animation: fadeInUp 0.3s ease;
|
||||
|
||||
@keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
125
storefront/src/components/ui/ui-language-switcher.vue
Normal file
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
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>
|
||||
59
storefront/src/components/ui/ui-loader.vue
Normal file
59
storefront/src/components/ui/ui-loader.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div class="loader">
|
||||
<li class="loader__dots" id="dot-1"></li>
|
||||
<li class="loader__dots" id="dot-2"></li>
|
||||
<li class="loader__dots" id="dot-3"></li>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loader {
|
||||
display: flex;
|
||||
gap: 0.6em;
|
||||
list-style: none;
|
||||
|
||||
&__dots {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
#dot-1 {
|
||||
animation: loader-1 0.6s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes loader-1 {
|
||||
50% {
|
||||
opacity: 0;
|
||||
transform: translateY(-0.3em);
|
||||
}
|
||||
}
|
||||
|
||||
#dot-2 {
|
||||
animation: loader-2 0.6s 0.3s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes loader-2 {
|
||||
50% {
|
||||
opacity: 0;
|
||||
transform: translateY(-0.3em);
|
||||
}
|
||||
}
|
||||
|
||||
#dot-3 {
|
||||
animation: loader-3 0.6s 0.6s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes loader-3 {
|
||||
50% {
|
||||
opacity: 0;
|
||||
transform: translateY(-0.3em);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
90
storefront/src/components/ui/ui-textarea.vue
Normal file
90
storefront/src/components/ui/ui-textarea.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<div class="block">
|
||||
<textarea
|
||||
:placeholder="placeholder"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
class="block__textarea"
|
||||
/>
|
||||
<p v-if="!validate" class="block__error">{{ errorMessage }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
|
||||
const $emit = defineEmits()
|
||||
const props = defineProps({
|
||||
placeholder: String,
|
||||
isError: Boolean,
|
||||
error: String,
|
||||
modelValue: [String, Number],
|
||||
rules: Array
|
||||
})
|
||||
|
||||
const validate = ref(true)
|
||||
const errorMessage = ref('')
|
||||
const onInput = (e) => {
|
||||
let result = true
|
||||
|
||||
props.rules?.forEach((rule) => {
|
||||
result = rule((e.target).value)
|
||||
|
||||
if (result !== true) {
|
||||
errorMessage.value = String(result)
|
||||
result = false
|
||||
}
|
||||
})
|
||||
|
||||
validate.value = result
|
||||
|
||||
return $emit('update:modelValue', (e.target).value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.block {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
|
||||
&__textarea {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
resize: none;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid $black;
|
||||
background-color: $white;
|
||||
|
||||
color: #1f1f1f;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
&::placeholder {
|
||||
color: #2B2B2B;
|
||||
}
|
||||
}
|
||||
|
||||
&__error {
|
||||
color: $error;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
animation: fadeInUp 0.3s ease;
|
||||
|
||||
@keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
6
storefront/src/composables/auth/index.js
Normal file
6
storefront/src/composables/auth/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export * from './useLogin.js';
|
||||
export * from './useLogout.js';
|
||||
export * from './useNewPassword.js';
|
||||
export * from './usePasswordReset.js';
|
||||
export * from './useRefresh.js';
|
||||
export * from './useRegister.js';
|
||||
93
storefront/src/composables/auth/useLogin.js
Normal file
93
storefront/src/composables/auth/useLogin.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {LOGIN} from "@/graphql/mutations/auth.js";
|
||||
import {ref} from "vue";
|
||||
import {ElNotification} from "element-plus";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useUserStore} from "@/stores/user.js";
|
||||
import translations from "@/core/helpers/translations.js";
|
||||
import {
|
||||
DEFAULT_LOCALE, LOCALE_STORAGE_ACCESS_TOKEN_KEY,
|
||||
LOCALE_STORAGE_LOCALE_KEY,
|
||||
LOCALE_STORAGE_REFRESH_TOKEN_KEY,
|
||||
} from "@/config/index.js";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {usePendingOrder} from "@/composables/orders";
|
||||
import {useWishlist} from "@/composables/wishlist";
|
||||
|
||||
export function useLogin() {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore()
|
||||
const {t} = useI18n();
|
||||
|
||||
const { mutate: loginMutation } = useMutation(LOGIN);
|
||||
|
||||
const { getPendingOrder } = usePendingOrder();
|
||||
const { getWishlist } = useWishlist();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function login(
|
||||
email,
|
||||
password,
|
||||
isStayLogin
|
||||
) {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await loginMutation({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
if (isStayLogin) {
|
||||
localStorage.setItem(LOCALE_STORAGE_REFRESH_TOKEN_KEY, response.data.obtainJwtToken.refreshToken)
|
||||
}
|
||||
|
||||
if (response.data?.obtainJwtToken) {
|
||||
userStore.setUser({
|
||||
user: response.data.obtainJwtToken.user
|
||||
});
|
||||
localStorage.setItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY, response.data.obtainJwtToken.accessToken)
|
||||
|
||||
ElNotification({
|
||||
message: t('popup.success.login'),
|
||||
type: 'success'
|
||||
})
|
||||
|
||||
await router.push({
|
||||
name: 'home',
|
||||
params: {
|
||||
locale: localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.obtainJwtToken.user.language !== translations.currentLocale) {
|
||||
translations.switchLanguage(response.data.obtainJwtToken.user.language, router, route)
|
||||
}
|
||||
|
||||
await getPendingOrder(response.data.obtainJwtToken.user.email);
|
||||
await getWishlist();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("useLogin 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 {
|
||||
login,
|
||||
loading
|
||||
};
|
||||
}
|
||||
33
storefront/src/composables/auth/useLogout.js
Normal file
33
storefront/src/composables/auth/useLogout.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import {useUserStore} from "@/stores/user.js";
|
||||
import {
|
||||
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 userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
async function logout() {
|
||||
userStore.setUser({
|
||||
user: null,
|
||||
accessToken: null
|
||||
})
|
||||
|
||||
localStorage.removeItem(LOCALE_STORAGE_REFRESH_TOKEN_KEY)
|
||||
localStorage.removeItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY)
|
||||
|
||||
await router.push({
|
||||
name: 'home',
|
||||
params: {
|
||||
locale: localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
logout
|
||||
};
|
||||
}
|
||||
73
storefront/src/composables/auth/useNewPassword.js
Normal file
73
storefront/src/composables/auth/useNewPassword.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {NEW_PASSWORD} from "@/graphql/mutations/auth.js";
|
||||
import {computed, ref} from "vue";
|
||||
import {ElNotification} from "element-plus";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY} from "@/config/index.js";
|
||||
|
||||
export function useNewPassword() {
|
||||
const {t} = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const { mutate: newPasswordMutation } = useMutation(NEW_PASSWORD);
|
||||
|
||||
const token = computed(() =>
|
||||
route.query.token ? (route.query.token) : undefined,
|
||||
);
|
||||
const uid = computed(() =>
|
||||
route.query.uid ? (route.query.uid) : undefined,
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function newPassword(
|
||||
password,
|
||||
confirmPassword
|
||||
) {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await newPasswordMutation({
|
||||
password,
|
||||
confirmPassword,
|
||||
token: token.value,
|
||||
uid: uid.value
|
||||
});
|
||||
|
||||
if (response.data?.confirmResetPassword.success) {
|
||||
ElNotification({
|
||||
message: t('popup.success.newPassword'),
|
||||
type: 'success'
|
||||
})
|
||||
|
||||
await router.push({
|
||||
name: 'home',
|
||||
params: {
|
||||
locale: localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("useNewPassword 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 {
|
||||
newPassword,
|
||||
loading
|
||||
};
|
||||
}
|
||||
51
storefront/src/composables/auth/usePasswordReset.js
Normal file
51
storefront/src/composables/auth/usePasswordReset.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {RESET_PASSWORD} from "@/graphql/mutations/auth.js";
|
||||
import {ref} from "vue";
|
||||
import {ElNotification} from "element-plus";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
export function usePasswordReset() {
|
||||
const {t} = useI18n();
|
||||
|
||||
const { mutate: resetPasswordMutation } = useMutation(RESET_PASSWORD);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function resetPassword(
|
||||
email
|
||||
) {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await resetPasswordMutation({
|
||||
email
|
||||
});
|
||||
|
||||
if (response.data?.resetPassword.success) {
|
||||
ElNotification({
|
||||
message: t('popup.success.reset'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("usePasswordReset 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 {
|
||||
resetPassword,
|
||||
loading
|
||||
};
|
||||
}
|
||||
74
storefront/src/composables/auth/useRefresh.js
Normal file
74
storefront/src/composables/auth/useRefresh.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {REFRESH} from "@/graphql/mutations/auth.js";
|
||||
import {computed, ref} from "vue";
|
||||
import {ElNotification} from "element-plus";
|
||||
import {useI18n} from "vue-i18n";
|
||||
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";
|
||||
import {useWishlist} from "@/composables/wishlist";
|
||||
|
||||
export function useRefresh() {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const {t} = useI18n();
|
||||
|
||||
const { mutate: refreshMutation } = useMutation(REFRESH);
|
||||
|
||||
const { getPendingOrder } = usePendingOrder();
|
||||
const { getWishlist } = useWishlist();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true;
|
||||
|
||||
const refreshToken = computed(() => localStorage.getItem(LOCALE_STORAGE_REFRESH_TOKEN_KEY))
|
||||
|
||||
if (!refreshToken.value) return
|
||||
|
||||
try {
|
||||
const response = await refreshMutation({
|
||||
refreshToken: refreshToken.value
|
||||
});
|
||||
|
||||
if (response.data?.refreshJwtToken) {
|
||||
userStore.setUser({
|
||||
user: response.data.refreshJwtToken.user,
|
||||
accessToken: response.data.refreshJwtToken.accessToken
|
||||
})
|
||||
|
||||
if (response.data.refreshJwtToken.user.language !== translations.currentLocale) {
|
||||
translations.switchLanguage(response.data.refreshJwtToken.user.language, router, route)
|
||||
}
|
||||
|
||||
localStorage.setItem(LOCALE_STORAGE_REFRESH_TOKEN_KEY, response.data.refreshJwtToken.refreshToken)
|
||||
|
||||
await getPendingOrder(response.data.refreshJwtToken.user.email);
|
||||
await getWishlist();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("useRefresh 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 {
|
||||
refresh,
|
||||
loading
|
||||
};
|
||||
}
|
||||
85
storefront/src/composables/auth/useRegister.js
Normal file
85
storefront/src/composables/auth/useRegister.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {REGISTER} from "@/graphql/mutations/auth.js";
|
||||
import {h, ref} from "vue";
|
||||
import {ElNotification} from "element-plus";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useMailClient} from "@/composables/utils";
|
||||
|
||||
export function useRegister() {
|
||||
const {t} = useI18n();
|
||||
|
||||
const { mutate: registerMutation } = useMutation(REGISTER);
|
||||
|
||||
const { mailClientUrl, detectMailClient, openMailClient } = useMailClient();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function register(
|
||||
firstName,
|
||||
lastName,
|
||||
phoneNumber,
|
||||
email,
|
||||
password,
|
||||
confirmPassword
|
||||
) {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await registerMutation({
|
||||
firstName,
|
||||
lastName,
|
||||
phoneNumber,
|
||||
email,
|
||||
password,
|
||||
confirmPassword
|
||||
});
|
||||
|
||||
if (response.data?.createUser?.success) {
|
||||
detectMailClient(email);
|
||||
|
||||
ElNotification({
|
||||
message: h('div', [
|
||||
h('p', t('popup.success.register')),
|
||||
mailClientUrl.value ? h(
|
||||
'button',
|
||||
{
|
||||
style: {
|
||||
marginTop: '10px',
|
||||
padding: '6px 12px',
|
||||
backgroundColor: '#000000',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
onClick: () => {
|
||||
openMailClient()
|
||||
}
|
||||
},
|
||||
t('buttons.goEmail')
|
||||
) : ''
|
||||
]),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("useRegister 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 {
|
||||
register,
|
||||
loading
|
||||
};
|
||||
}
|
||||
1
storefront/src/composables/blog/index.js
Normal file
1
storefront/src/composables/blog/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './usePosts.js'
|
||||
24
storefront/src/composables/blog/usePostBySlug.js
Normal file
24
storefront/src/composables/blog/usePostBySlug.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
import {GET_POST_BY_SLUG} from "@/graphql/queries/blog.js";
|
||||
import {computed} from "vue";
|
||||
|
||||
export function usePostbySlug() {
|
||||
const { result, loading, error, load } = useLazyQuery(GET_POST_BY_SLUG);
|
||||
|
||||
const post = computed(() => result.value?.posts.edges[0].node ?? []);
|
||||
|
||||
if (error.value) {
|
||||
console.error("usePostbySlug error:", error.value);
|
||||
}
|
||||
|
||||
const getPost = (slug) => {
|
||||
return load(null, { slug });
|
||||
};
|
||||
|
||||
return {
|
||||
post,
|
||||
loading,
|
||||
error,
|
||||
getPost
|
||||
};
|
||||
}
|
||||
20
storefront/src/composables/blog/usePosts.js
Normal file
20
storefront/src/composables/blog/usePosts.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
import { GET_POSTS } from "@/graphql/queries/blog.js";
|
||||
import {computed} from "vue";
|
||||
|
||||
export function usePosts() {
|
||||
const { result, loading, error, load } = useLazyQuery(GET_POSTS);
|
||||
|
||||
const posts = computed(() => result.value?.posts.edges ?? []);
|
||||
|
||||
if (error.value) {
|
||||
console.error("usePosts error:", error.value);
|
||||
}
|
||||
|
||||
return {
|
||||
posts,
|
||||
loading,
|
||||
error,
|
||||
getPosts: load
|
||||
};
|
||||
}
|
||||
2
storefront/src/composables/brands/index.js
Normal file
2
storefront/src/composables/brands/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './useBrands.js'
|
||||
export * from './useBrandByUuid.js'
|
||||
24
storefront/src/composables/brands/useBrandByUuid.js
Normal file
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
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
storefront/src/composables/categories/index.js
Normal file
2
storefront/src/composables/categories/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './useCategories.js'
|
||||
export * from './useCategorybySlug.js'
|
||||
20
storefront/src/composables/categories/useCategories.js
Normal file
20
storefront/src/composables/categories/useCategories.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
import {computed} from "vue";
|
||||
import {GET_CATEGORIES} from "@/graphql/queries/categories.js";
|
||||
|
||||
export function useCategories() {
|
||||
const { result, loading, error, load } = useLazyQuery(GET_CATEGORIES);
|
||||
|
||||
const categories = computed(() => result.value?.categories.edges ?? []);
|
||||
|
||||
if (error.value) {
|
||||
console.error("useCategories error:", error.value);
|
||||
}
|
||||
|
||||
return {
|
||||
categories,
|
||||
loading,
|
||||
error,
|
||||
getCategories: load
|
||||
};
|
||||
}
|
||||
24
storefront/src/composables/categories/useCategorybySlug.js
Normal file
24
storefront/src/composables/categories/useCategorybySlug.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
import {computed} from "vue";
|
||||
import {GET_CATEGORY_BY_SLUG} from "@/graphql/queries/categories.js";
|
||||
|
||||
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("useCategorybySlug error:", error.value);
|
||||
}
|
||||
|
||||
const getCategory = (slug) => {
|
||||
return load(null, { slug });
|
||||
};
|
||||
|
||||
return {
|
||||
category,
|
||||
loading,
|
||||
error,
|
||||
getCategory
|
||||
};
|
||||
}
|
||||
1
storefront/src/composables/company/index.js
Normal file
1
storefront/src/composables/company/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './useCompanyInfo.js';
|
||||
26
storefront/src/composables/company/useCompanyInfo.js
Normal file
26
storefront/src/composables/company/useCompanyInfo.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import {useLazyQuery} from "@vue/apollo-composable";
|
||||
import {GET_COMPANY_INFO} from "@/graphql/queries/company.js";
|
||||
import {useCompanyStore} from "@/stores/company.js";
|
||||
import {watchEffect} from "vue";
|
||||
|
||||
export function useCompanyInfo() {
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const { result, loading, error, load } = useLazyQuery(GET_COMPANY_INFO);
|
||||
|
||||
if (error.value) {
|
||||
console.error("useCompanyInfo error:", error.value);
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (result.value?.parameters) {
|
||||
companyStore.setCompanyInfo(result.value.parameters);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
getCompanyInfo: load
|
||||
};
|
||||
}
|
||||
1
storefront/src/composables/contact/index.js
Normal file
1
storefront/src/composables/contact/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './useContactUs.js'
|
||||
59
storefront/src/composables/contact/useContactUs.js
Normal file
59
storefront/src/composables/contact/useContactUs.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {ref} from "vue";
|
||||
import {ElNotification} from "element-plus";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {CONTACT_US} from "@/graphql/mutations/contact.js";
|
||||
|
||||
export function useContactUs() {
|
||||
const {t} = useI18n();
|
||||
|
||||
const { mutate: contactUsMutation } = useMutation(CONTACT_US);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function contactUs(
|
||||
name,
|
||||
email,
|
||||
phoneNumber,
|
||||
subject,
|
||||
message
|
||||
) {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await contactUsMutation({
|
||||
name,
|
||||
email,
|
||||
phoneNumber,
|
||||
subject,
|
||||
message
|
||||
});
|
||||
|
||||
if (response.data?.contactUs.received) {
|
||||
ElNotification({
|
||||
message: t('popup.success.contactUs'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("useContactUs 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 {
|
||||
contactUs,
|
||||
loading
|
||||
};
|
||||
}
|
||||
2
storefront/src/composables/languages/index.js
Normal file
2
storefront/src/composables/languages/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './useLanguageSwitch.js'
|
||||
export * from './useLanguages.js'
|
||||
62
storefront/src/composables/languages/useLanguageSwitch.js
Normal file
62
storefront/src/composables/languages/useLanguageSwitch.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {computed, ref} from "vue";
|
||||
import {ElNotification} from "element-plus";
|
||||
import {useI18n} from "vue-i18n";
|
||||
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 userStore = useUserStore()
|
||||
const {t} = useI18n();
|
||||
|
||||
const { mutate: languageSwitchMutation } = useMutation(SWITCH_LANGUAGE);
|
||||
|
||||
const accessToken = computed(() => localStorage.getItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY))
|
||||
const userUuid = computed(() => userStore.user?.uuid)
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function switchLanguage(
|
||||
locale
|
||||
) {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
translations.switchLanguage(locale)
|
||||
if (accessToken.value) {
|
||||
const response = await languageSwitchMutation(
|
||||
userUuid.value,
|
||||
locale
|
||||
);
|
||||
|
||||
if (response.data?.updateUser) {
|
||||
userStore.setUser({
|
||||
user: response.data.updateUser.user,
|
||||
accessToken: accessToken.value
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("useLanguageSet 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 {
|
||||
switchLanguage,
|
||||
loading
|
||||
};
|
||||
}
|
||||
35
storefront/src/composables/languages/useLanguages.js
Normal file
35
storefront/src/composables/languages/useLanguages.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
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 {LOCALE_STORAGE_LOCALE_KEY, SUPPORTED_LOCALES} from "@/config/index.js";
|
||||
|
||||
export function useLanguages() {
|
||||
const languageStore = useLanguageStore()
|
||||
|
||||
const { result, loading, error, load } = useLazyQuery(GET_LANGUAGES);
|
||||
|
||||
if (error.value) {
|
||||
console.error("useLanguages error:", error.value);
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (result.value?.languages) {
|
||||
languageStore.setLanguages(
|
||||
result.value.languages.filter((locale) =>
|
||||
SUPPORTED_LOCALES.some(supportedLocale =>
|
||||
supportedLocale.code === locale.code
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
languageStore.setCurrentLocale(languageStore.languages.find((locale) => locale.code === localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY)))
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
getLanguages: load
|
||||
};
|
||||
}
|
||||
1
storefront/src/composables/orders/index.js
Normal file
1
storefront/src/composables/orders/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './usePendingOrder.js';
|
||||
24
storefront/src/composables/orders/usePendingOrder.js
Normal file
24
storefront/src/composables/orders/usePendingOrder.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {GET_ORDERS} from "@/graphql/queries/orders.js";
|
||||
import {useCartStore} from "@/stores/cart.js";
|
||||
|
||||
export function usePendingOrder() {
|
||||
const cartStore = useCartStore()
|
||||
|
||||
const { mutate: pendingOrderMutation } = useMutation(GET_ORDERS);
|
||||
|
||||
async function getPendingOrder(userEmail) {
|
||||
const response = await pendingOrderMutation({
|
||||
status: "PENDING",
|
||||
userEmail
|
||||
});
|
||||
|
||||
if (!response.errors) {
|
||||
cartStore.setCurrentOrders(response.data.orders.edges[0].node)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getPendingOrder
|
||||
};
|
||||
}
|
||||
3
storefront/src/composables/products/index.js
Normal file
3
storefront/src/composables/products/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './useProducts.js'
|
||||
export * from './useProductBySlug.js'
|
||||
export * from './useProductTags.js'
|
||||
24
storefront/src/composables/products/useProductBySlug.js
Normal file
24
storefront/src/composables/products/useProductBySlug.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
import {computed} from "vue";
|
||||
import {GET_PRODUCT_BY_SLUG} from "@/graphql/queries/products.js";
|
||||
|
||||
export function useProductbySlug() {
|
||||
const { result, loading, error, load } = useLazyQuery(GET_PRODUCT_BY_SLUG);
|
||||
|
||||
const product = computed(() => result.value?.products.edges[0].node ?? []);
|
||||
|
||||
if (error.value) {
|
||||
console.error("useProductbySlug error:", error.value);
|
||||
}
|
||||
|
||||
const getProduct = (slug) => {
|
||||
return load(null, { slug });
|
||||
};
|
||||
|
||||
return {
|
||||
product,
|
||||
loading,
|
||||
error,
|
||||
getProduct
|
||||
};
|
||||
}
|
||||
20
storefront/src/composables/products/useProductTags.js
Normal file
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
|
||||
};
|
||||
}
|
||||
47
storefront/src/composables/products/useProducts.js
Normal file
47
storefront/src/composables/products/useProducts.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
import { computed, ref } from "vue";
|
||||
import { GET_PRODUCTS } from "@/graphql/queries/products.js";
|
||||
|
||||
export function useProducts() {
|
||||
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 = {}) => {
|
||||
const newVariables = { ...variables.value };
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
newVariables[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
variables.value = newVariables;
|
||||
|
||||
if (result.value) {
|
||||
await refetch();
|
||||
} else {
|
||||
await load();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
products,
|
||||
pageInfo,
|
||||
loading,
|
||||
error,
|
||||
getProducts
|
||||
};
|
||||
}
|
||||
2
storefront/src/composables/search/index.js
Normal file
2
storefront/src/composables/search/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './useSearch.js'
|
||||
export * from './useSearchUi.js'
|
||||
52
storefront/src/composables/search/useSearch.js
Normal file
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
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
|
||||
};
|
||||
}
|
||||
3
storefront/src/composables/user/index.js
Normal file
3
storefront/src/composables/user/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './useUserUpdating.js';
|
||||
export * from './useUserActivation.js';
|
||||
export * from '../languages/useLanguageSwitch.js';
|
||||
48
storefront/src/composables/user/useDeposit.js
Normal file
48
storefront/src/composables/user/useDeposit.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {ref} from "vue";
|
||||
import {ElNotification} from "element-plus";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {DEPOSIT} from "@/graphql/mutations/deposit.js";
|
||||
|
||||
export function useDeposit() {
|
||||
const {t} = useI18n();
|
||||
|
||||
const { mutate: depositMutation } = useMutation(DEPOSIT);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function deposit(
|
||||
amount
|
||||
) {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await depositMutation(
|
||||
amount
|
||||
);
|
||||
|
||||
if (response.data?.deposit) {
|
||||
window.open(response.data.deposit.transaction.process.url)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("useDeposit 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 {
|
||||
deposit,
|
||||
loading
|
||||
};
|
||||
}
|
||||
59
storefront/src/composables/user/useUserActivation.js
Normal file
59
storefront/src/composables/user/useUserActivation.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {computed, ref} from "vue";
|
||||
import {ElNotification} from "element-plus";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useRoute} from "vue-router";
|
||||
import {ACTIVATE_USER} from "@/graphql/mutations/user.js";
|
||||
|
||||
export function useUserActivation() {
|
||||
const {t} = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const { mutate: userActivationMutation } = useMutation(ACTIVATE_USER);
|
||||
|
||||
const token = computed(() =>
|
||||
route.query.token ? (route.query.token) : undefined,
|
||||
);
|
||||
const uid = computed(() =>
|
||||
route.query.uid ? (route.query.uid) : undefined,
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function activateUser() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await userActivationMutation({
|
||||
token: token.value,
|
||||
uid: uid.value
|
||||
});
|
||||
|
||||
if (response.data?.activateUser) {
|
||||
ElNotification({
|
||||
message: t("popup.activationSuccess"),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("useUserActivation 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 {
|
||||
activateUser,
|
||||
loading
|
||||
};
|
||||
}
|
||||
121
storefront/src/composables/user/useUserUpdating.js
Normal file
121
storefront/src/composables/user/useUserUpdating.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {computed, ref} from "vue";
|
||||
import {ElNotification} from "element-plus";
|
||||
import {useI18n} from "vue-i18n";
|
||||
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 userStore = useUserStore()
|
||||
const {t} = useI18n();
|
||||
|
||||
const { mutate: userUpdatingMutation } = useMutation(UPDATE_USER);
|
||||
|
||||
const { logout } = useLogout();
|
||||
|
||||
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);
|
||||
|
||||
async function updateUser(
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
phoneNumber,
|
||||
password,
|
||||
confirmPassword
|
||||
) {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const fields = {
|
||||
uuid: userUuid.value,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
phoneNumber,
|
||||
password,
|
||||
confirmPassword
|
||||
};
|
||||
|
||||
const params = Object.fromEntries(
|
||||
Object.entries(fields).filter(([_, value]) =>
|
||||
value !== undefined && value !== null && value !== ''
|
||||
)
|
||||
);
|
||||
|
||||
// if (('password' in params && !('passwordConfirm' in params)) ||
|
||||
// (!('password' in params) && 'passwordConfirm' in params)) {
|
||||
// ElNotification({
|
||||
// title: t('popup.errors.main'),
|
||||
// message: t('popup.errors.noDataToUpdate'),
|
||||
// type: 'error'
|
||||
// });
|
||||
// }
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
ElNotification({
|
||||
title: t('popup.errors.main'),
|
||||
message: t('popup.errors.noDataToUpdate'),
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
const response = await userUpdatingMutation(
|
||||
params
|
||||
);
|
||||
|
||||
if (response.data?.updateUser) {
|
||||
if (userEmail.value !== email) {
|
||||
await logout()
|
||||
|
||||
ElNotification({
|
||||
message: t("popup.success.confirmEmail"),
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
userStore.setUser({
|
||||
user: response.data.updateUser.user,
|
||||
accessToken: accessToken.value
|
||||
})
|
||||
|
||||
ElNotification({
|
||||
message: t("popup.successUpdate"),
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (response.data.updateUser.user.language !== translations.currentLocale) {
|
||||
translations.switchLanguage(response.data.updateUser.user.language, router, route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("useUserUpdating 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 {
|
||||
updateUser,
|
||||
loading
|
||||
};
|
||||
}
|
||||
1
storefront/src/composables/utils/index.js
Normal file
1
storefront/src/composables/utils/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './useMainClient.js';
|
||||
42
storefront/src/composables/utils/useMainClient.js
Normal file
42
storefront/src/composables/utils/useMainClient.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { ref } from 'vue';
|
||||
|
||||
export function useMailClient() {
|
||||
const mailClientUrl = ref(null);
|
||||
|
||||
const mailClients = {
|
||||
'gmail.com': 'https://mail.google.com/',
|
||||
'outlook.com': 'https://outlook.live.com/',
|
||||
'icloud.com': 'https://www.icloud.com/mail/',
|
||||
'yahoo.com': 'https://mail.yahoo.com/',
|
||||
'mail.ru': 'https://e.mail.ru/inbox/',
|
||||
'yandex.ru': 'https://mail.yandex.ru/',
|
||||
'proton.me': 'https://account.proton.me/mail',
|
||||
'fastmail.com': 'https://fastmail.com/'
|
||||
};
|
||||
|
||||
function detectMailClient(email) {
|
||||
mailClientUrl.value = null;
|
||||
|
||||
if (!email) return;
|
||||
|
||||
const domain = email.split('@')[1];
|
||||
|
||||
Object.entries(mailClients).forEach((el) => {
|
||||
if (domain === el[0]) mailClientUrl.value = el[1];
|
||||
});
|
||||
|
||||
return mailClientUrl.value;
|
||||
}
|
||||
|
||||
function openMailClient() {
|
||||
if (mailClientUrl.value) {
|
||||
window.open(mailClientUrl.value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mailClientUrl,
|
||||
detectMailClient,
|
||||
openMailClient
|
||||
};
|
||||
}
|
||||
1
storefront/src/composables/wishlist/index.js
Normal file
1
storefront/src/composables/wishlist/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './useWishlist.js';
|
||||
21
storefront/src/composables/wishlist/useWishlist.js
Normal file
21
storefront/src/composables/wishlist/useWishlist.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import {useMutation} from "@vue/apollo-composable";
|
||||
import {GET_WISHLIST} from "@/graphql/queries/wishlist.js";
|
||||
import {useWishlistStore} from "@/stores/wishlist.js";
|
||||
|
||||
export function useWishlist() {
|
||||
const wishlistStore = useWishlistStore()
|
||||
|
||||
const { mutate: wishlistMutation } = useMutation(GET_WISHLIST);
|
||||
|
||||
async function getWishlist() {
|
||||
const response = await wishlistMutation();
|
||||
|
||||
if (!response.errors) {
|
||||
wishlistStore.setWishlist(response.data.wishlists.edges[0].node)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getWishlist
|
||||
};
|
||||
}
|
||||
88
storefront/src/config/index.js
Normal file
88
storefront/src/config/index.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// APP
|
||||
|
||||
export const APP_NAME = import.meta.env.EVIBES_PROJECT_NAME
|
||||
|
||||
export const APP_NAME_KEY = APP_NAME.toLowerCase()
|
||||
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
]
|
||||
|
||||
export const DEFAULT_LOCALE = SUPPORTED_LOCALES.find(locale => locale.default)?.code || 'en-gb'
|
||||
|
||||
|
||||
|
||||
// LOCAL STORAGE
|
||||
|
||||
export const LOCALE_STORAGE_LOCALE_KEY = `${APP_NAME_KEY}-user-locale`;
|
||||
|
||||
export const LOCALE_STORAGE_REFRESH_TOKEN_KEY = `${APP_NAME_KEY}-refresh`;
|
||||
|
||||
export const LOCALE_STORAGE_ACCESS_TOKEN_KEY = `${APP_NAME_KEY}-access`;
|
||||
30
storefront/src/core/helpers/i18n-utils.js
Normal file
30
storefront/src/core/helpers/i18n-utils.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
export async function loadLocaleMessages(locale) {
|
||||
try {
|
||||
const messages = await import(`../locales/${locale}.json`)
|
||||
return messages.default || messages
|
||||
} catch (error) {
|
||||
console.error(`Не удалось загрузить локаль: ${locale}`, error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocaleFilename(localeCode, localesConfig) {
|
||||
const localeInfo = localesConfig.find(locale => locale.code === localeCode)
|
||||
return localeInfo?.file || `${localeCode}.json`
|
||||
}
|
||||
|
||||
export async function loadAllLocaleMessages(supportedLocales) {
|
||||
const messages = {}
|
||||
|
||||
for (const locale of supportedLocales) {
|
||||
try {
|
||||
const localeMessages = await import(`../../locales/${locale.code}.json`)
|
||||
messages[locale.code] = localeMessages.default || localeMessages
|
||||
} catch (error) {
|
||||
console.error(`Не удалось загрузить локаль: ${locale.code}`, error)
|
||||
messages[locale.code] = {}
|
||||
}
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
107
storefront/src/core/helpers/translations.js
Normal file
107
storefront/src/core/helpers/translations.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import i18n from '@/core/plugins/i18n.config';
|
||||
import {DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY, SUPPORTED_LOCALES} from "@/config/index.js";
|
||||
|
||||
const translations = {
|
||||
get currentLocale() {
|
||||
return i18n.global.locale.value
|
||||
},
|
||||
|
||||
set currentLocale(newLocale) {
|
||||
i18n.global.locale.value = newLocale
|
||||
},
|
||||
|
||||
switchLanguage(newLocale, router = null, route = null) {
|
||||
translations.currentLocale = newLocale
|
||||
|
||||
document.querySelector('html').setAttribute('lang', newLocale)
|
||||
|
||||
localStorage.setItem(LOCALE_STORAGE_LOCALE_KEY, newLocale)
|
||||
|
||||
if (router && route) {
|
||||
const newRoute = {
|
||||
...route,
|
||||
params: {
|
||||
...route.params,
|
||||
locale: newLocale
|
||||
}
|
||||
};
|
||||
|
||||
router.push(newRoute).catch(err => {
|
||||
if (err.name !== 'NavigationDuplicated') {
|
||||
console.error('Navigation error:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
isLocaleSupported(locale) {
|
||||
if (locale) {
|
||||
return SUPPORTED_LOCALES.some(supportedLocale => supportedLocale.code === locale);
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
getUserLocale() {
|
||||
const locale =
|
||||
window.navigator.language ||
|
||||
DEFAULT_LOCALE.code
|
||||
|
||||
return {
|
||||
locale: locale,
|
||||
localeNoRegion: locale.split('-')[0]
|
||||
}
|
||||
},
|
||||
|
||||
getPersistedLocale() {
|
||||
const persistedLocale = localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY)
|
||||
|
||||
if (translations.isLocaleSupported(persistedLocale)) {
|
||||
return persistedLocale
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
guessDefaultLocale() {
|
||||
const userPersistedLocale = translations.getPersistedLocale()
|
||||
if (userPersistedLocale) {
|
||||
return userPersistedLocale
|
||||
}
|
||||
|
||||
const userPreferredLocale = translations.getUserLocale()
|
||||
|
||||
if (translations.isLocaleSupported(userPreferredLocale.locale)) {
|
||||
return userPreferredLocale.locale
|
||||
}
|
||||
|
||||
if (translations.isLocaleSupported(userPreferredLocale.localeNoRegion)) {
|
||||
return userPreferredLocale.localeNoRegion
|
||||
}
|
||||
|
||||
return DEFAULT_LOCALE.code
|
||||
},
|
||||
|
||||
async routeMiddleware(to, _from, next) {
|
||||
const paramLocale = to.params.locale
|
||||
|
||||
if (!translations.isLocaleSupported(paramLocale)) {
|
||||
return next(translations.guessDefaultLocale())
|
||||
}
|
||||
|
||||
await translations.switchLanguage(paramLocale)
|
||||
|
||||
return next()
|
||||
},
|
||||
|
||||
i18nRoute(to) {
|
||||
return {
|
||||
...to,
|
||||
params: {
|
||||
locale: translations.currentLocale,
|
||||
...to.params
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default translations
|
||||
33
storefront/src/core/plugins/i18n.config.js
Normal file
33
storefront/src/core/plugins/i18n.config.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { createI18n } from 'vue-i18n'
|
||||
import {DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY, SUPPORTED_LOCALES} from "@/config/index.js";
|
||||
import {loadAllLocaleMessages} from "@/core/helpers/i18n-utils.js";
|
||||
|
||||
const savedLocale = localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY)
|
||||
const currentLocale = savedLocale && SUPPORTED_LOCALES.some(locale => locale.code === savedLocale)
|
||||
? savedLocale
|
||||
: DEFAULT_LOCALE
|
||||
|
||||
if (!savedLocale) {
|
||||
localStorage.setItem(LOCALE_STORAGE_LOCALE_KEY, DEFAULT_LOCALE)
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: currentLocale,
|
||||
fallbackLocale: DEFAULT_LOCALE,
|
||||
allowComposition: true,
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
messages: {}
|
||||
})
|
||||
|
||||
export async function setupI18n() {
|
||||
const messages = await loadAllLocaleMessages(SUPPORTED_LOCALES)
|
||||
|
||||
Object.keys(messages).forEach(locale => {
|
||||
i18n.global.setLocaleMessage(locale, messages[locale])
|
||||
})
|
||||
|
||||
return i18n
|
||||
}
|
||||
|
||||
export default i18n
|
||||
42
storefront/src/core/rules/textFieldRules.js
Normal file
42
storefront/src/core/rules/textFieldRules.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import i18n from '@/core/plugins/i18n.config'
|
||||
|
||||
const isEmail = (email) => {
|
||||
if (!email) return required(email);
|
||||
if (/.+@.+\..+/.test(email)) return true
|
||||
const { t } = i18n.global
|
||||
return t('errors.mail')
|
||||
}
|
||||
|
||||
const required = (text) => {
|
||||
if (text) return true
|
||||
const { t } = i18n.global
|
||||
return t('errors.required')
|
||||
}
|
||||
|
||||
const isPasswordValid = (pass) => {
|
||||
const { t } = i18n.global
|
||||
|
||||
if (pass.length < 8) {
|
||||
return t('errors.needMin')
|
||||
}
|
||||
|
||||
if (!/[a-z]/.test(pass)) {
|
||||
return t('errors.needLower')
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(pass)) {
|
||||
return t('errors.needUpper')
|
||||
}
|
||||
|
||||
if (!/\d/.test(pass)) {
|
||||
return t('errors.needNumber')
|
||||
}
|
||||
|
||||
if (!/[#.?!@$%^&*'()_+=:;"'/>.<,|\-]/.test(pass)) {
|
||||
return t('errors.needSpecial')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export { required, isEmail, isPasswordValid }
|
||||
8
storefront/src/graphql/fragments/brands.fragment.js
Normal file
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
|
||||
}
|
||||
`
|
||||
11
storefront/src/graphql/fragments/categories.fragment.js
Normal file
11
storefront/src/graphql/fragments/categories.fragment.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import gql from 'graphql-tag'
|
||||
|
||||
export const CATEGORY_FRAGMENT = gql`
|
||||
fragment Category on CategoryType {
|
||||
name
|
||||
uuid
|
||||
image
|
||||
description
|
||||
slug
|
||||
}
|
||||
`
|
||||
28
storefront/src/graphql/fragments/orders.fragment.js
Normal file
28
storefront/src/graphql/fragments/orders.fragment.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import gql from 'graphql-tag'
|
||||
import {PRODUCT_FRAGMENT} from "@/graphql/fragments/products.fragment.js";
|
||||
|
||||
export const ORDER_FRAGMENT = gql`
|
||||
fragment Order on OrderType {
|
||||
totalPrice
|
||||
uuid
|
||||
status
|
||||
buyTime
|
||||
totalPrice
|
||||
humanReadableId
|
||||
orderProducts {
|
||||
edges {
|
||||
node {
|
||||
uuid
|
||||
notifications
|
||||
attributes
|
||||
quantity
|
||||
status
|
||||
product {
|
||||
...Product
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${PRODUCT_FRAGMENT}
|
||||
`
|
||||
53
storefront/src/graphql/fragments/products.fragment.js
Normal file
53
storefront/src/graphql/fragments/products.fragment.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import gql from 'graphql-tag'
|
||||
|
||||
export const PRODUCT_FRAGMENT = gql`
|
||||
fragment Product on ProductType {
|
||||
uuid
|
||||
name
|
||||
price
|
||||
quantity
|
||||
slug
|
||||
category {
|
||||
name
|
||||
}
|
||||
images {
|
||||
edges {
|
||||
node {
|
||||
image
|
||||
}
|
||||
}
|
||||
}
|
||||
attributeGroups {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
uuid
|
||||
attributes {
|
||||
name
|
||||
uuid
|
||||
values {
|
||||
value
|
||||
uuid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
feedbacks {
|
||||
edges {
|
||||
node {
|
||||
uuid
|
||||
rating
|
||||
}
|
||||
}
|
||||
}
|
||||
tags {
|
||||
edges {
|
||||
node {
|
||||
tagName
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
17
storefront/src/graphql/fragments/user.fragment.js
Normal file
17
storefront/src/graphql/fragments/user.fragment.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import gql from 'graphql-tag'
|
||||
|
||||
export const USER_FRAGMENT = gql`
|
||||
fragment User on UserType {
|
||||
avatar
|
||||
uuid
|
||||
attributes
|
||||
language
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phoneNumber
|
||||
balance {
|
||||
amount
|
||||
}
|
||||
}
|
||||
`
|
||||
16
storefront/src/graphql/fragments/wishlist.fragment.js
Normal file
16
storefront/src/graphql/fragments/wishlist.fragment.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import gql from 'graphql-tag'
|
||||
import {PRODUCT_FRAGMENT} from "@/graphql/fragments/products.fragment.js";
|
||||
|
||||
export const WISHLIST_FRAGMENT = gql`
|
||||
fragment Wishlist on WishlistType {
|
||||
uuid
|
||||
products {
|
||||
edges {
|
||||
node {
|
||||
...Product
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${PRODUCT_FRAGMENT}
|
||||
`
|
||||
90
storefront/src/graphql/mutations/auth.js
Normal file
90
storefront/src/graphql/mutations/auth.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import gql from 'graphql-tag'
|
||||
import {USER_FRAGMENT} from "@/graphql/fragments/user.fragment.js";
|
||||
|
||||
export const REGISTER = gql`
|
||||
mutation register(
|
||||
$firstName: String!,
|
||||
$lastName: String!,
|
||||
$email: String!,
|
||||
$phoneNumber: String!,
|
||||
$password: String!,
|
||||
$confirmPassword: String!
|
||||
) {
|
||||
createUser(
|
||||
firstName: $firstName,
|
||||
lastName: $lastName,
|
||||
email: $email,
|
||||
phoneNumber: $phoneNumber,
|
||||
password: $password,
|
||||
confirmPassword: $confirmPassword
|
||||
) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const LOGIN = gql`
|
||||
mutation login(
|
||||
$email: String!,
|
||||
$password: String!
|
||||
) {
|
||||
obtainJwtToken(
|
||||
email: $email,
|
||||
password: $password
|
||||
) {
|
||||
accessToken
|
||||
refreshToken
|
||||
user {
|
||||
...User
|
||||
}
|
||||
}
|
||||
}
|
||||
${USER_FRAGMENT}
|
||||
`
|
||||
|
||||
export const REFRESH = gql`
|
||||
mutation refresh(
|
||||
$refreshToken: String!
|
||||
) {
|
||||
refreshJwtToken(
|
||||
refreshToken: $refreshToken
|
||||
) {
|
||||
accessToken
|
||||
refreshToken
|
||||
user {
|
||||
...User
|
||||
}
|
||||
}
|
||||
}
|
||||
${USER_FRAGMENT}
|
||||
`
|
||||
|
||||
export const RESET_PASSWORD = gql`
|
||||
mutation resetPassword(
|
||||
$email: String!,
|
||||
) {
|
||||
resetPassword(
|
||||
email: $email,
|
||||
) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const NEW_PASSWORD = gql`
|
||||
mutation confirmResetPassword(
|
||||
$password: String!,
|
||||
$confirmPassword: String!,
|
||||
$token: String!,
|
||||
$uid: String!,
|
||||
) {
|
||||
confirmResetPassword(
|
||||
password: $password,
|
||||
confirmPassword: $confirmPassword,
|
||||
token: $token,
|
||||
uid: $uid
|
||||
) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`
|
||||
68
storefront/src/graphql/mutations/cart.js
Normal file
68
storefront/src/graphql/mutations/cart.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import gql from 'graphql-tag'
|
||||
import {ORDER_FRAGMENT} from "@/graphql/fragments/orders.fragment.js";
|
||||
|
||||
export const ADD_TO_CART = gql`
|
||||
mutation addToCart(
|
||||
$orderUuid: String!,
|
||||
$productUuid: String!
|
||||
) {
|
||||
addOrderProduct(
|
||||
orderUuid: $orderUuid,
|
||||
productUuid: $productUuid
|
||||
) {
|
||||
order {
|
||||
...Order
|
||||
}
|
||||
}
|
||||
}
|
||||
${ORDER_FRAGMENT}
|
||||
`
|
||||
|
||||
export const REMOVE_FROM_CART = gql`
|
||||
mutation removeFromCart(
|
||||
$orderUuid: String!,
|
||||
$productUuid: String!
|
||||
) {
|
||||
removeOrderProduct(
|
||||
orderUuid: $orderUuid,
|
||||
productUuid: $productUuid
|
||||
) {
|
||||
order {
|
||||
...Order
|
||||
}
|
||||
}
|
||||
}
|
||||
${ORDER_FRAGMENT}
|
||||
`
|
||||
|
||||
export const REMOVE_KIND_FROM_CART = gql`
|
||||
mutation removeKindFromCart(
|
||||
$orderUuid: String!,
|
||||
$productUuid: String!
|
||||
) {
|
||||
removeOrderProductsOfAKind(
|
||||
orderUuid: $orderUuid,
|
||||
productUuid: $productUuid
|
||||
) {
|
||||
order {
|
||||
...Order
|
||||
}
|
||||
}
|
||||
}
|
||||
${ORDER_FRAGMENT}
|
||||
`
|
||||
|
||||
export const REMOVE_ALL_FROM_CART = gql`
|
||||
mutation removeAllFromCart(
|
||||
$orderUuid: String!
|
||||
) {
|
||||
removeAllOrderProducts(
|
||||
orderUuid: $orderUuid
|
||||
) {
|
||||
order {
|
||||
...Order
|
||||
}
|
||||
}
|
||||
}
|
||||
${ORDER_FRAGMENT}
|
||||
`
|
||||
22
storefront/src/graphql/mutations/contact.js
Normal file
22
storefront/src/graphql/mutations/contact.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import gql from 'graphql-tag'
|
||||
|
||||
export const CONTACT_US = gql`
|
||||
mutation contactUs(
|
||||
$name: String!,
|
||||
$email: String!,
|
||||
$phoneNumber: String,
|
||||
$subject: String!,
|
||||
$message: String!,
|
||||
) {
|
||||
contactUs(
|
||||
name: $name,
|
||||
email: $email,
|
||||
phoneNumber: $phoneNumber,
|
||||
subject: $subject,
|
||||
message: $message
|
||||
) {
|
||||
error
|
||||
received
|
||||
}
|
||||
}
|
||||
`
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue