Merge branch 'storefront'
# Conflicts: # storefront/package-lock.json # storefront/package.json # storefront/public/favicon.ico # storefront/src/App.vue # storefront/src/apollo/index.js # storefront/src/components/base/header/base-header-search.vue # storefront/src/components/base/header/base-header.vue # storefront/src/components/forms/deposit-form.vue # storefront/src/components/forms/login-form.vue # storefront/src/components/forms/new-password-form.vue # storefront/src/components/forms/register-form.vue # storefront/src/components/forms/reset-password-form.vue # storefront/src/components/forms/update-form.vue # storefront/src/components/home/home-hero.vue # storefront/src/components/ui/ui-button.vue # storefront/src/components/ui/ui-checkbox.vue # storefront/src/components/ui/ui-input.vue # storefront/src/components/ui/ui-language-switcher.vue # storefront/src/composables/auth/useLogin.js # storefront/src/composables/auth/useLogout.js # storefront/src/composables/auth/useRefresh.js # storefront/src/composables/categories/useCategorybySlug.js # storefront/src/composables/languages/useLanguageSwitch.js # storefront/src/composables/languages/useLanguages.js # storefront/src/composables/products/index.js # storefront/src/composables/products/useProducts.js # storefront/src/composables/search/useSearch.js # storefront/src/composables/search/useSearchUi.js # storefront/src/composables/user/useUserUpdating.js # storefront/src/config/index.js # storefront/src/locales/de-de.json # storefront/src/main.js # storefront/src/pages/home-page.vue # storefront/src/pages/product-page.vue # storefront/src/pages/store-page.vue # storefront/src/router/index.js # storefront/src/stores/languages.js # storefront/src/stores/user.js
This commit is contained in:
commit
70594a891e
54 changed files with 3238 additions and 4969 deletions
5422
storefront/package-lock.json
generated
5422
storefront/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,34 +1,33 @@
|
|||
{
|
||||
"name": "astro",
|
||||
"name": "evibes-frontend",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.2.2",
|
||||
"@astrojs/vue": "^5.1.0",
|
||||
"@nanostores/vue": "^1.0.0",
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@vue/apollo-composable": "^4.2.2",
|
||||
"@vueuse/core": "^13.2.0",
|
||||
"@vueuse/integrations": "^13.3.0",
|
||||
"astro": "^5.8.1",
|
||||
"element-plus": "^2.9.11",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-request": "^7.2.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"nanostores": "^1.0.1",
|
||||
"pinia": "^3.0.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"swiper": "^11.2.8",
|
||||
"universal-cookie": "^7.2.2",
|
||||
"vue": "^3.5.16",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.4",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue3-marquee-slider": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"sass": "^1.83.0",
|
||||
"sass-loader": "^16.0.4"
|
||||
"sass-loader": "^16.0.4",
|
||||
"vite": "^6.2.4",
|
||||
"vite-plugin-vue-devtools": "^7.7.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
storefront/public/favicon.ico
Normal file
BIN
storefront/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
65
storefront/src/App.vue
Normal file
65
storefront/src/App.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
import {computed, onMounted} from "vue";
|
||||
import {useRefresh} from "@/composables/auth";
|
||||
import {useCompanyInfo} from "@/composables/company";
|
||||
import {useLanguages} from "@/composables/languages/index.js";
|
||||
import BaseHeader from "@/components/base/header/base-header.vue";
|
||||
import BaseFooter from "@/components/base/base-footer.vue";
|
||||
import BaseAuth from "@/components/base/base-auth.vue";
|
||||
import LoginForm from "@/components/forms/login-form.vue";
|
||||
import RegisterForm from "@/components/forms/register-form.vue";
|
||||
import NewPasswordForm from "@/components/forms/new-password-form.vue";
|
||||
import ResetPasswordForm from "@/components/forms/reset-password-form.vue";
|
||||
import {useAppStore} from "@/stores/app.js";
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const activeState = computed(() => appStore.activeState)
|
||||
|
||||
const { refresh } = useRefresh();
|
||||
const { getCompanyInfo } = useCompanyInfo();
|
||||
const { getLanguages } = useLanguages();
|
||||
|
||||
onMounted(async () => {
|
||||
await refresh()
|
||||
await getCompanyInfo()
|
||||
await getLanguages()
|
||||
|
||||
setInterval(async () => {
|
||||
await refresh()
|
||||
}, 600000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="main" id="top">
|
||||
<base-header />
|
||||
<Transition name="opacity" mode="out-in">
|
||||
<base-auth v-if="activeState">
|
||||
<login-form v-if="activeState === 'login'" />
|
||||
<register-form v-if="activeState === 'register'" />
|
||||
<reset-password-form v-if="activeState === 'reset-password'" />
|
||||
<new-password-form v-if="activeState === 'new-password'" />
|
||||
</base-auth>
|
||||
</Transition>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="opacity" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
<base-footer />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
padding-top: 90px;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
:deep(.el-skeleton__item) {
|
||||
--el-skeleton-color: #d0d2d3 !important;
|
||||
--el-skeleton-to-color: #b4b4b7 !important;
|
||||
}
|
||||
</style>
|
||||
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',
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
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 |
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>
|
||||
|
|
@ -5,8 +5,7 @@
|
|||
class="search__wrapper"
|
||||
:class="[{ active: isSearchActive }]"
|
||||
>
|
||||
<!-- <form class="search__form" @submit.prevent="submitSearch">-->
|
||||
<form class="search__form" @submit.prevent="">
|
||||
<form class="search__form" @submit.prevent="submitSearch">
|
||||
<input
|
||||
type="text"
|
||||
v-model="query"
|
||||
|
|
@ -50,7 +49,6 @@
|
|||
</div>
|
||||
<div class="search__results-empty" v-if="!hasResults && query && !loading">
|
||||
<p>{{ t('header.search.empty') }}</p>
|
||||
<!-- <p>header.search.empty</p>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -63,13 +61,13 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import HeaderSearchSkeleton from "@/components/skeletons/header/header-search-skeleton.vue";
|
||||
import {useSearchUI} from "@/composables/search/index.js";
|
||||
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, locale} = useI18n()
|
||||
|
||||
console.log(locale.value)
|
||||
const {t} = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
query,
|
||||
|
|
@ -83,16 +81,16 @@ const {
|
|||
toggleSearch
|
||||
} = useSearchUI();
|
||||
|
||||
// function submitSearch() {
|
||||
// if (query.value) {
|
||||
// router.push({
|
||||
// name: 'search',
|
||||
// query: { q: query.value }
|
||||
// });
|
||||
//
|
||||
// toggleSearch(false);
|
||||
// }
|
||||
// }
|
||||
function submitSearch() {
|
||||
if (query.value) {
|
||||
router.push({
|
||||
name: 'search',
|
||||
query: { q: query.value }
|
||||
});
|
||||
|
||||
toggleSearch(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -1,87 +1,74 @@
|
|||
<template>
|
||||
<header class="header">
|
||||
<a href="/">
|
||||
<img class="header__logo" src="/images/evibes-big-simple.png" alt="logo">
|
||||
</a>
|
||||
<p>{{ t('field.search') }}</p>
|
||||
<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>-->
|
||||
<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 {useI18n} from "vue-i18n";
|
||||
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 {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";
|
||||
import { createI18n, useI18n } from 'vue-i18n';
|
||||
|
||||
//TODO: add categories to header
|
||||
|
||||
const props = defineProps({
|
||||
locale: String,
|
||||
messages: Object
|
||||
const {t} = useI18n()
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const isAuthenticated = computed(() => userStore.user)
|
||||
|
||||
const { categories, loading, getCategories } = useCategories();
|
||||
|
||||
onMounted(async () => {
|
||||
await getCategories()
|
||||
})
|
||||
|
||||
// const i18n = createI18n({
|
||||
// locale: props.locale,
|
||||
// messages: { [props.locale]: props.messages },
|
||||
// });
|
||||
|
||||
const { t } = useI18n({ useScope: 'global', locale: props.locale, messages: { [props.locale]: props.messages } });
|
||||
|
||||
// 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>
|
||||
|
|
|
|||
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>
|
||||
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>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="hero">
|
||||
<div class="container">
|
||||
<div class="hero__wrapper">
|
||||
<img src="/images/evibes-big.png" alt="logo">
|
||||
<img src="@images/evibes-big.png" alt="logo">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.hero {
|
||||
background-image: url(/images/homeBg.png);
|
||||
background-image: url(@images/homeBg.png);
|
||||
background-repeat: no-repeat;
|
||||
-webkit-background-size: cover;
|
||||
background-size: cover;
|
||||
|
|
|
|||
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>
|
||||
|
|
@ -29,18 +29,16 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {computed, ref} from "vue";
|
||||
import {onClickOutside} from "@vueuse/core";
|
||||
import {useLanguages, useLanguageSwitch} from "@/composables/languages/index.js";
|
||||
import {useLanguageStore} from "@/stores/languages.js";
|
||||
import {useLanguageSwitch} from "@/composables/languages/index.js";
|
||||
import LanguageSwitcherSkeleton from "@/components/skeletons/ui/language-switcher-skeleton.vue";
|
||||
import {COOKIES_LOCALE_KEY, SUPPORTED_LOCALES} from "@/config/index.js";
|
||||
import {useCookies} from "@vueuse/integrations/useCookies";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
const cookie = useCookies(['/', null, null])
|
||||
const { t } = useI18n();
|
||||
const locales = ref([])
|
||||
const currentLocale = computed(() => locales.value.find((locale) => locale.code === cookie.get(COOKIES_LOCALE_KEY)))
|
||||
const languageStore = useLanguageStore()
|
||||
|
||||
const locales = computed(() => languageStore.languages)
|
||||
const currentLocale = computed(() => languageStore.currentLocale)
|
||||
|
||||
const isSwitcherVisible = ref(false)
|
||||
const setSwitcherVisible = (state) => {
|
||||
|
|
@ -50,43 +48,7 @@ const setSwitcherVisible = (state) => {
|
|||
const switcherRef = ref(null)
|
||||
onClickOutside(switcherRef, () => isSwitcherVisible.value = false)
|
||||
|
||||
const { languages, getLanguages } = useLanguages()
|
||||
const { switchLanguage } = useLanguageSwitch()
|
||||
|
||||
// const switchLanguage = async (localeCode) => {
|
||||
// setSwitcherVisible(false);
|
||||
//
|
||||
// if (!SUPPORTED_LOCALES.some(locale => locale.code === localeCode)) {
|
||||
// console.error(`Locale ${localeCode} is not supported`);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// cookie.set(COOKIES_LOCALE_KEY, localeCode);
|
||||
//
|
||||
// locale.value = localeCode;
|
||||
//
|
||||
// await switchUserLanguage(localeCode);
|
||||
//
|
||||
// const currentPath = window.location.pathname;
|
||||
// const pathParts = currentPath.split('/');
|
||||
//
|
||||
// if (pathParts.length > 1 && SUPPORTED_LOCALES.some(locale => locale.code === pathParts[1])) {
|
||||
// pathParts[1] = localeCode;
|
||||
// } else {
|
||||
// pathParts.splice(1, 0, localeCode);
|
||||
// }
|
||||
//
|
||||
// window.location.href = pathParts.join('/');
|
||||
// } catch (error) {
|
||||
// console.error('Error switching language:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
onMounted( async () => {
|
||||
await getLanguages()
|
||||
locales.value = languages.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
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>
|
||||
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
|
||||
};
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
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'
|
||||
export * from './useBrandByUuid'
|
||||
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
|
||||
};
|
||||
}
|
||||
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,93 +1,62 @@
|
|||
import {computed, ref} from 'vue';
|
||||
import { request } from 'graphql-request';
|
||||
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 {useCookies} from "@vueuse/integrations/useCookies";
|
||||
import {APP_API_DOMAIN, COOKIES_ACCESS_TOKEN_KEY, COOKIES_LOCALE_KEY, SUPPORTED_LOCALES} from "@/config/index.js";
|
||||
import { useStore } from '@nanostores/vue';
|
||||
import {setUser, user} from '@/stores/user';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {LOCALE_STORAGE_ACCESS_TOKEN_KEY} from "@/config/index.js";
|
||||
|
||||
export function useLanguageSwitch() {
|
||||
const cookie = useCookies(['/', null, null])
|
||||
const $user = useStore(user);
|
||||
const { locale } = useI18n();
|
||||
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 userUuid = computed(() => $user.uuid)
|
||||
const accessToken = computed(() => cookie.get(COOKIES_ACCESS_TOKEN_KEY))
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
const updateUserLanguageOnServer = async (localeCode) => {
|
||||
async function switchLanguage(
|
||||
locale
|
||||
) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
translations.switchLanguage(locale)
|
||||
if (accessToken.value) {
|
||||
const variables = {
|
||||
userUuid: userUuid.value,
|
||||
locale: localeCode
|
||||
};
|
||||
|
||||
const data = await request(
|
||||
APP_API_DOMAIN,
|
||||
SWITCH_LANGUAGE,
|
||||
variables
|
||||
const response = await languageSwitchMutation(
|
||||
userUuid.value,
|
||||
locale
|
||||
);
|
||||
|
||||
if (data?.updateUser) {
|
||||
setUser(data.updateUser.user)
|
||||
if (response.data?.updateUser) {
|
||||
userStore.setUser({
|
||||
user: response.data.updateUser.user,
|
||||
accessToken: accessToken.value
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("useLanguageSet error:", error);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
console.error('useLanguageSwitch error:', err);
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const switchLanguage = async (localeCode) => {
|
||||
if (!SUPPORTED_LOCALES.some(locale => locale.code === localeCode)) {
|
||||
console.error(`Locale ${localeCode} is not supported`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
cookie.set(COOKIES_LOCALE_KEY, localeCode);
|
||||
|
||||
locale.value = localeCode;
|
||||
|
||||
if (accessToken.value && userUuid.value) {
|
||||
await updateUserLanguageOnServer(localeCode);
|
||||
}
|
||||
|
||||
const currentPath = window.location.pathname;
|
||||
const pathParts = currentPath.split('/');
|
||||
|
||||
if (pathParts.length > 1 && SUPPORTED_LOCALES.some(locale => locale.code === pathParts[1])) {
|
||||
pathParts[1] = localeCode;
|
||||
} else {
|
||||
pathParts.splice(1, 0, localeCode);
|
||||
}
|
||||
|
||||
window.location.href = pathParts.join('/');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error switching language:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
switchLanguage,
|
||||
updateUserLanguageOnServer
|
||||
loading
|
||||
};
|
||||
}
|
||||
|
|
@ -1,37 +1,35 @@
|
|||
import {ref} from "vue";
|
||||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
import {watchEffect} from "vue";
|
||||
import {GET_LANGUAGES} from "@/graphql/queries/languages.js";
|
||||
import {APP_API_DOMAIN, SUPPORTED_LOCALES} from "@/config/index.js";
|
||||
import {request} from "graphql-request";
|
||||
import {useLanguageStore} from "@/stores/languages.js";
|
||||
import {LOCALE_STORAGE_LOCALE_KEY, SUPPORTED_LOCALES} from "@/config/index.js";
|
||||
|
||||
export function useLanguages() {
|
||||
const languages = ref([])
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const languageStore = useLanguageStore()
|
||||
|
||||
const getLanguages = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
const { result, loading, error, load } = useLazyQuery(GET_LANGUAGES);
|
||||
|
||||
try {
|
||||
const data = await request(APP_API_DOMAIN, GET_LANGUAGES);
|
||||
if (error.value) {
|
||||
console.error("useLanguages error:", error.value);
|
||||
}
|
||||
|
||||
languages.value = data.languages.filter((locale) =>
|
||||
watchEffect(() => {
|
||||
if (result.value?.languages) {
|
||||
languageStore.setLanguages(
|
||||
result.value.languages.filter((locale) =>
|
||||
SUPPORTED_LOCALES.some(supportedLocale =>
|
||||
supportedLocale.code === locale.code
|
||||
)
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
console.error('useLanguage error:', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
|
||||
languageStore.setCurrentLocale(languageStore.languages.find((locale) => locale.code === localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY)))
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
languages,
|
||||
loading,
|
||||
error,
|
||||
getLanguages
|
||||
getLanguages: load
|
||||
};
|
||||
}
|
||||
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'
|
||||
export * from './useProductBySlug'
|
||||
export * from './useProductTags'
|
||||
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
|
||||
};
|
||||
}
|
||||
|
|
@ -1,40 +1,52 @@
|
|||
import { ref } from 'vue';
|
||||
import { request } from 'graphql-request';
|
||||
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";
|
||||
import {APP_API_DOMAIN} from "@/config/index.js";
|
||||
|
||||
export function useSearch() {
|
||||
const searchResults = ref(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const {t} = useI18n();
|
||||
|
||||
const search = async (query) => {
|
||||
const { mutate: searchMutation } = useMutation(SEARCH);
|
||||
|
||||
const loading = ref(false);
|
||||
const searchResults = ref(null);
|
||||
|
||||
async function search(
|
||||
query
|
||||
) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
searchResults.value = null;
|
||||
|
||||
try {
|
||||
const variables = { query };
|
||||
const data = await request(
|
||||
APP_API_DOMAIN,
|
||||
SEARCH,
|
||||
variables
|
||||
);
|
||||
const response = await searchMutation({
|
||||
query
|
||||
});
|
||||
|
||||
searchResults.value = data.search.results;
|
||||
return data.search.results;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
console.error('useSearch error:', err);
|
||||
return null;
|
||||
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 {
|
||||
searchResults,
|
||||
search,
|
||||
loading,
|
||||
error,
|
||||
search
|
||||
searchResults
|
||||
};
|
||||
}
|
||||
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,13 +1,9 @@
|
|||
// APP
|
||||
|
||||
// export const APP_NAME = import.meta.env.EVIBES_PROJECT_NAME
|
||||
export const APP_NAME = "Flaucards"
|
||||
export const APP_NAME = import.meta.env.EVIBES_PROJECT_NAME
|
||||
|
||||
export const APP_NAME_KEY = APP_NAME.toLowerCase()
|
||||
|
||||
// export const APP_API_DOMAIN = 'https://api.' + import.meta.env.EVIBES_BASE_DOMAIN + '/graphql/'
|
||||
export const APP_API_DOMAIN = 'https://api.' + 'flaucards.com' + '/graphql/'
|
||||
|
||||
|
||||
|
||||
// LOCALES
|
||||
|
|
@ -83,10 +79,10 @@ export const DEFAULT_LOCALE = SUPPORTED_LOCALES.find(locale => locale.default)?.
|
|||
|
||||
|
||||
|
||||
// COOKIES
|
||||
// LOCAL STORAGE
|
||||
|
||||
export const COOKIES_LOCALE_KEY = `${APP_NAME_KEY}-user-locale`;
|
||||
export const LOCALE_STORAGE_LOCALE_KEY = `${APP_NAME_KEY}-user-locale`;
|
||||
|
||||
export const COOKIES_REFRESH_TOKEN_KEY = `${APP_NAME_KEY}-refresh`;
|
||||
export const LOCALE_STORAGE_REFRESH_TOKEN_KEY = `${APP_NAME_KEY}-refresh`;
|
||||
|
||||
export const COOKIES_ACCESS_TOKEN_KEY = `${APP_NAME_KEY}-access`;
|
||||
export const LOCALE_STORAGE_ACCESS_TOKEN_KEY = `${APP_NAME_KEY}-access`;
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
{
|
||||
"fields": {
|
||||
"search": "Suche"
|
||||
}
|
||||
|
||||
}
|
||||
33
storefront/src/main.js
Normal file
33
storefront/src/main.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import '@/assets/styles/global/fonts.scss'
|
||||
import '@/assets/styles/main.scss'
|
||||
import 'primeicons/primeicons.css'
|
||||
import {createApp, h, provide} from 'vue'
|
||||
import { DefaultApolloClient } from '@vue/apollo-composable'
|
||||
import { createApolloClient } from './apollo'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import {setupI18n} from "@/core/plugins/i18n.config.js";
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
|
||||
const pinia = createPinia()
|
||||
const i18n = await setupI18n()
|
||||
|
||||
const app = createApp({
|
||||
setup() {
|
||||
const apolloClient = createApolloClient()
|
||||
|
||||
provide(DefaultApolloClient, apolloClient)
|
||||
},
|
||||
render: () => h(App)
|
||||
})
|
||||
|
||||
app
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.use(router)
|
||||
.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
19
storefront/src/pages/cart-page.vue
Normal file
19
storefront/src/pages/cart-page.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<div class="cart">
|
||||
<div class="container">
|
||||
<div class="cart__wrapper">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cart {
|
||||
|
||||
}
|
||||
</style>
|
||||
40
storefront/src/pages/home-page.vue
Normal file
40
storefront/src/pages/home-page.vue
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div class="home">
|
||||
<home-hero />
|
||||
<home-brands />
|
||||
<home-collection />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useUserActivation} from "@/composables/user";
|
||||
import {useAppStore} from "@/stores/app.js";
|
||||
import HomeHero from "@/components/home/home-hero.vue";
|
||||
import HomeCollection from "@/components/home/home-collection.vue";
|
||||
import HomeBrands from "@/components/home/home-brands.vue";
|
||||
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const { activateUser } = useUserActivation();
|
||||
|
||||
onMounted( async () => {
|
||||
if (route.name === "activate-user") {
|
||||
await activateUser()
|
||||
}
|
||||
|
||||
if (route.name === "reset-password") {
|
||||
await appStore.setActiveState('new-password')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 125px;
|
||||
}
|
||||
</style>
|
||||
29
storefront/src/pages/product-page.vue
Normal file
29
storefront/src/pages/product-page.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="product">
|
||||
<div class="container">
|
||||
<div class="product__wrapper">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onMounted} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useProductbySlug} from "@/composables/products";
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const slug = computed(() => route.params.productSlug)
|
||||
|
||||
const { product, loading, getProduct } = useProductbySlug();
|
||||
|
||||
onMounted(async () => {
|
||||
await getProduct(slug.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
24
storefront/src/pages/search-page.vue
Normal file
24
storefront/src/pages/search-page.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<div class="search">
|
||||
<div class="container">
|
||||
<div class="search__wrapper">
|
||||
<h1>{{ query }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const query = computed(() => route.query.q)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
|
||||
}
|
||||
</style>
|
||||
26
storefront/src/pages/store-page.vue
Normal file
26
storefront/src/pages/store-page.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<div class="store">
|
||||
<div class="container">
|
||||
<div class="store__wrapper">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted} from "vue";
|
||||
import {useProducts} from "@/composables/products/index.js";
|
||||
|
||||
const { products, pageInfo, loading, getProducts } = useProducts();
|
||||
|
||||
onMounted(async () => {
|
||||
await getProducts({})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.store {
|
||||
|
||||
}
|
||||
</style>
|
||||
19
storefront/src/pages/wishlist-page.vue
Normal file
19
storefront/src/pages/wishlist-page.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<div class="wishlist">
|
||||
<div class="container">
|
||||
<div class="wishlist__wrapper">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wishlist {
|
||||
|
||||
}
|
||||
</style>
|
||||
143
storefront/src/router/index.js
Normal file
143
storefront/src/router/index.js
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import {createRouter, createWebHistory, RouterView} from 'vue-router'
|
||||
import HomePage from "@/pages/home-page.vue";
|
||||
import translation from "@/core/helpers/translations.js";
|
||||
import {APP_NAME} from "@/config/index.js";
|
||||
import NewPasswordForm from "@/components/forms/new-password-form.vue";
|
||||
import BlogPage from "@/pages/blog-page.vue";
|
||||
import PostPage from "@/pages/post-page.vue";
|
||||
import ProfilePage from "@/pages/profile-page.vue";
|
||||
import {useUserStore} from "@/stores/user.js";
|
||||
import RegisterForm from "@/components/forms/register-form.vue";
|
||||
import LoginForm from "@/components/forms/login-form.vue";
|
||||
import ResetPasswordForm from "@/components/forms/reset-password-form.vue";
|
||||
import StorePage from "@/pages/store-page.vue";
|
||||
import ProductPage from "@/pages/product-page.vue";
|
||||
import SearchPage from "@/pages/search-page.vue";
|
||||
import CartPage from "@/pages/cart-page.vue";
|
||||
import WishlistPage from "@/pages/wishlist-page.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/:locale?',
|
||||
component: RouterView,
|
||||
beforeEnter: translation.routeMiddleware,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'home',
|
||||
component: HomePage,
|
||||
meta: {
|
||||
title: "Home"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'activate-user',
|
||||
name: 'activate-user',
|
||||
component: HomePage,
|
||||
meta: {
|
||||
title: 'Home'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'blog',
|
||||
name: 'blog',
|
||||
component: BlogPage,
|
||||
meta: {
|
||||
title: 'Blog'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'blog/post/:postSlug',
|
||||
name: 'blog-post',
|
||||
component: PostPage,
|
||||
meta: {
|
||||
title: 'Post'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'store',
|
||||
name: 'store',
|
||||
component: StorePage,
|
||||
meta: {
|
||||
title: 'Store'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'product/:productSlug',
|
||||
name: 'product',
|
||||
component: ProductPage,
|
||||
meta: {
|
||||
title: 'Product'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
name: 'search',
|
||||
component: SearchPage,
|
||||
meta: {
|
||||
title: 'Search'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'cart',
|
||||
name: 'cart',
|
||||
component: CartPage,
|
||||
meta: {
|
||||
title: 'Cart'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wishlist',
|
||||
name: 'wishlist',
|
||||
component: WishlistPage,
|
||||
meta: {
|
||||
title: 'Wishlist'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
name: 'profile',
|
||||
component: ProfilePage,
|
||||
meta: {
|
||||
title: 'Profile',
|
||||
requiresAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes,
|
||||
scrollBehavior() {
|
||||
document.querySelector('#top').scrollIntoView({ behavior: 'smooth' })
|
||||
return {
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const userStore = useUserStore();
|
||||
const isAuthenticated = userStore.accessToken
|
||||
|
||||
document.title = to.meta.title ? `${APP_NAME} | ` + to.meta?.title : APP_NAME
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
return next({
|
||||
name: 'home',
|
||||
query: { redirect: to.fullPath }
|
||||
});
|
||||
}
|
||||
|
||||
if (to.meta.requiresGuest && isAuthenticated) {
|
||||
return next({ name: 'home' });
|
||||
}
|
||||
|
||||
next();
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
@ -1,23 +1,21 @@
|
|||
import { map } from 'nanostores';
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const languages = map({
|
||||
languages: [],
|
||||
currentLocale: null
|
||||
});
|
||||
export const useLanguageStore = defineStore('language', () => {
|
||||
const languages = ref([]);
|
||||
const setLanguages = (payload) => {
|
||||
languages.value = payload
|
||||
};
|
||||
|
||||
export function setLanguages(languagesList) {
|
||||
languages.setKey('languages', languagesList);
|
||||
const currentLocale = ref(null);
|
||||
const setCurrentLocale = (payload) => {
|
||||
currentLocale.value = payload
|
||||
};
|
||||
|
||||
if (!languages.get().currentLocale && languagesList.length > 0) {
|
||||
languages.setKey('currentLocale', languagesList[0]);
|
||||
}
|
||||
}
|
||||
|
||||
export function setCurrentLocale(locale) {
|
||||
const allLanguages = languages.get().languages;
|
||||
const selectedLocale = allLanguages.find(l => l.code === locale);
|
||||
|
||||
if (selectedLocale) {
|
||||
languages.setKey('currentLocale', selectedLocale);
|
||||
}
|
||||
return {
|
||||
languages,
|
||||
setLanguages,
|
||||
currentLocale,
|
||||
setCurrentLocale
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { atom } from 'nanostores';
|
||||
import {defineStore} from "pinia";
|
||||
import {ref} from "vue";
|
||||
|
||||
export const user = atom(null);
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const user = ref(null);
|
||||
|
||||
export function setUser(data) {
|
||||
user.set(data);
|
||||
const setUser = (payload) => {
|
||||
user.value = payload.user
|
||||
}
|
||||
|
||||
return { user, setUser }
|
||||
})
|
||||
Loading…
Reference in a new issue