Features: 1) Add Source Code Pro Light font asset for theme enhancement;
Fixes: None; Extra: None;
This commit is contained in:
parent
129ad1a6fa
commit
a31ee9c6b1
83 changed files with 15862 additions and 4 deletions
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-Black.ttf
Normal file
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-Black.ttf
Normal file
Binary file not shown.
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-Bold.ttf
Normal file
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-Bold.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-Light.ttf
Normal file
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-Light.ttf
Normal file
Binary file not shown.
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-Medium.ttf
Normal file
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-Medium.ttf
Normal file
Binary file not shown.
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-Regular.ttf
Normal file
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-Regular.ttf
Normal file
Binary file not shown.
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-SemiBold.ttf
Normal file
BIN
storefront/assets/fonts/SourceCodePro/SourceCodePro-SemiBold.ttf
Normal file
Binary file not shown.
42
storefront/assets/styles/global/fonts.scss
Normal file
42
storefront/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/assets/styles/global/mixins.scss
Normal file
7
storefront/assets/styles/global/mixins.scss
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
@mixin hover {
|
||||||
|
@media (hover: hover) and (pointer: fine) {
|
||||||
|
&:hover {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
storefront/assets/styles/global/variables.scss
Normal file
12
storefront/assets/styles/global/variables.scss
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
$font_default: 'Source Code Pro', sans-serif;
|
||||||
|
|
||||||
|
$white: #ffffff;
|
||||||
|
$light: #f8f7fc;
|
||||||
|
$black: #000000;
|
||||||
|
$accent: #7965d1;
|
||||||
|
$accentDark: #5743b5;
|
||||||
|
$accentLight: #a69cdc;
|
||||||
|
$accentDisabled: #826fa2;
|
||||||
|
$accentSmooth: #656bd1;
|
||||||
|
$error: #f13838;
|
||||||
|
$default_border_radius: 4px;
|
||||||
5
storefront/assets/styles/main.scss
Normal file
5
storefront/assets/styles/main.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
@use "modules/normalize";
|
||||||
|
@use "modules/transitions";
|
||||||
|
@use "global/mixins";
|
||||||
|
@use "global/variables";
|
||||||
|
@use "ui/collapse";
|
||||||
71
storefront/assets/styles/modules/normalize.scss
vendored
Normal file
71
storefront/assets/styles/modules/normalize.scss
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
@use "../global/variables" as *;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow-x: hidden;
|
||||||
|
font-family: $font_default;
|
||||||
|
word-spacing: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: $light;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-color: $accent $light;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--el-color-primary: #{$accent} !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-skeleton__item {
|
||||||
|
--el-skeleton-color: #c9ccd0 !important;
|
||||||
|
--el-skeleton-to-color: #c3c3c7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1680px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1300px) {
|
||||||
|
.container {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
storefront/assets/styles/modules/transitions.scss
Normal file
28
storefront/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;
|
||||||
|
}
|
||||||
63
storefront/components/base/auth.vue
Normal file
63
storefront/components/base/auth.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<div class="auth">
|
||||||
|
<div class="auth__content" ref="modalRef">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {onClickOutside} from "@vueuse/core";
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
appStore.unsetActiveState()
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
240
storefront/components/base/header/search.vue
Normal file
240
storefront/components/base/header/search.vue
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<Icon name="gridicons:cross" size="16" />
|
||||||
|
</button>
|
||||||
|
<div class="search__tools-line" v-if="query"></div>
|
||||||
|
<button type="submit">
|
||||||
|
<Icon name="tabler:search" size="16" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="search__results" :class="[{ active: (searchResults && isSearchActive) || loading }]">
|
||||||
|
<skeletons-header-search v-if="loading" />
|
||||||
|
<div
|
||||||
|
class="search__results-inner"
|
||||||
|
v-for="(blocks, 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">
|
||||||
|
<Icon name="ic:twotone-search" size="18" />
|
||||||
|
<p>{{ item.name }}</p>
|
||||||
|
</div>
|
||||||
|
<Icon name="line-md:external-link" size="18" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="search__results-empty" v-if="!hasResults && query && !loading">
|
||||||
|
<p>{{ t('header.search.empty') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<transition name="opacity" mode="out-in">
|
||||||
|
<div
|
||||||
|
class="search__bg"
|
||||||
|
@click="toggleSearch(false)"
|
||||||
|
v-if="isSearchActive"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useSearchUI } from "@/composables/search";
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
query,
|
||||||
|
isSearchActive,
|
||||||
|
loading,
|
||||||
|
searchResults,
|
||||||
|
filteredSearchResults,
|
||||||
|
hasResults,
|
||||||
|
getBlockTitle,
|
||||||
|
clearSearch,
|
||||||
|
toggleSearch
|
||||||
|
} = useSearchUI();
|
||||||
|
|
||||||
|
function submitSearch() {
|
||||||
|
if (query.value) {
|
||||||
|
router.push({
|
||||||
|
path: '/search',
|
||||||
|
query: { q: query.value }
|
||||||
|
})
|
||||||
|
|
||||||
|
toggleSearch(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
height: 45px;
|
||||||
|
|
||||||
|
&__bg {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
height: 100vh;
|
||||||
|
left: 0;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 1;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__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;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: #7c7c7c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
67
storefront/components/forms/deposit.vue
Normal file
67
storefront/components/forms/deposit.vue
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<!-- <form @submit.prevent="handleDeposit()" class="form">-->
|
||||||
|
<form @submit.prevent="" 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>-->
|
||||||
|
<ui-button
|
||||||
|
class="form__button"
|
||||||
|
:isDisabled="!isFormValid"
|
||||||
|
>
|
||||||
|
{{ t('buttons.topUp') }}
|
||||||
|
</ui-button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// import {useDeposit} from "@/composables/user/useDeposit.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>
|
||||||
90
storefront/components/forms/login.vue
Normal file
90
storefront/components/forms/login.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<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 lang="ts">
|
||||||
|
import {useLogin} from "~/composables/auth";
|
||||||
|
import {useValidators} from "~/composables/rules";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const { required, isEmail } = useValidators()
|
||||||
|
|
||||||
|
const email = ref<string>('');
|
||||||
|
const password = ref<string>('');
|
||||||
|
const isStayLogin = ref<boolean>(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>
|
||||||
70
storefront/components/forms/new-password.vue
Normal file
70
storefront/components/forms/new-password.vue
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<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 {useValidators} from "~/composables/rules/index.js";
|
||||||
|
import {useNewPassword} from "@/composables/auth";
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { isPasswordValid } = useValidators()
|
||||||
|
|
||||||
|
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>
|
||||||
133
storefront/components/forms/register.vue
Normal file
133
storefront/components/forms/register.vue
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
<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 {useValidators} from "~/composables/rules";
|
||||||
|
import {useRegister} from "~/composables/auth/index.js";
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const { required, isEmail, isPasswordValid } = useValidators()
|
||||||
|
|
||||||
|
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>
|
||||||
54
storefront/components/forms/reset-password.vue
Normal file
54
storefront/components/forms/reset-password.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<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 {useValidators} from "~/composables/rules";
|
||||||
|
import {usePasswordReset} from "@/composables/auth";
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { isEmail } = useValidators()
|
||||||
|
|
||||||
|
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>
|
||||||
107
storefront/components/forms/update.vue
Normal file
107
storefront/components/forms/update.vue
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
<template>
|
||||||
|
<!-- <form class="form" @submit.prevent="handleUpdate()">-->
|
||||||
|
<form class="form" @submit.prevent="">
|
||||||
|
<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>-->
|
||||||
|
<ui-button
|
||||||
|
class="form__button"
|
||||||
|
>
|
||||||
|
{{ t('buttons.save') }}
|
||||||
|
</ui-button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useValidators} from "~/composables/rules";
|
||||||
|
// import {useUserUpdating} from "@/composables/user";
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const { required, isEmail, isPasswordValid } = useValidators()
|
||||||
|
|
||||||
|
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>
|
||||||
91
storefront/components/home/collection/index.vue
Normal file
91
storefront/components/home/collection/index.vue
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<template>
|
||||||
|
<div class="collection">
|
||||||
|
<ui-title>{{ t('home.collection.title') }}</ui-title>
|
||||||
|
<div class="container">
|
||||||
|
<div class="collection__wrapper">
|
||||||
|
<div class="collection__inner">
|
||||||
|
<home-collection-inner
|
||||||
|
v-for="tag in tags"
|
||||||
|
:key="tag.node.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 lang="ts">
|
||||||
|
import {useProducts, useProductTags} from "@/composables/products";
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { tags } = await useProductTags();
|
||||||
|
const {
|
||||||
|
products: newProducts,
|
||||||
|
getProducts: getNewProducts
|
||||||
|
} = await useProducts();
|
||||||
|
|
||||||
|
const {
|
||||||
|
products: priceProducts,
|
||||||
|
getProducts: getPriceProducts
|
||||||
|
} = await useProducts();
|
||||||
|
|
||||||
|
const newProductsTag = computed(() => {
|
||||||
|
return {
|
||||||
|
name: t('home.collection.newTag'),
|
||||||
|
tagName: t('home.collection.newTag'),
|
||||||
|
uuid: 'new-products',
|
||||||
|
productSet: {
|
||||||
|
edges: newProducts.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const priceProductsTag = computed(() => {
|
||||||
|
return {
|
||||||
|
name: t('home.collection.cheapTag'),
|
||||||
|
tagName: t('home.collection.cheapTag'),
|
||||||
|
uuid: 'price-products',
|
||||||
|
productSet: {
|
||||||
|
edges: priceProducts.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
getNewProducts({
|
||||||
|
orderBy: '-modified'
|
||||||
|
}),
|
||||||
|
getPriceProducts({
|
||||||
|
orderBy: '-price'
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.collection {
|
||||||
|
&__wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
115
storefront/components/home/collection/inner.vue
Normal file
115
storefront/components/home/collection/inner.vue
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
<template>
|
||||||
|
<div class="tag" v-if="tag.productSet.edges.length">
|
||||||
|
<h2 class="tag__title">{{ tag.name }}</h2>
|
||||||
|
<div class="tag__block">
|
||||||
|
<div class="tag__block-inner">
|
||||||
|
<swiper
|
||||||
|
class="swiper"
|
||||||
|
v-if="tag.productSet.edges.length > 1"
|
||||||
|
: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"
|
||||||
|
>
|
||||||
|
<cards-product
|
||||||
|
class="swiper__slide-card"
|
||||||
|
:product="product.node"
|
||||||
|
/>
|
||||||
|
</swiper-slide>
|
||||||
|
</swiper>
|
||||||
|
<div class="tag__inner" v-else>
|
||||||
|
<cards-product
|
||||||
|
v-for="product in tag.productSet.edges"
|
||||||
|
:key="product.node.uuid"
|
||||||
|
:product="product.node"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/vue';
|
||||||
|
import { EffectCards, Mousewheel } from 'swiper/modules';
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/scrollbar';
|
||||||
|
import type {IProductTag} from "~/types";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tag: IProductTag;
|
||||||
|
}>();
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
width: 100%;
|
||||||
|
padding-block: 30px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper {
|
||||||
|
width: 100%;
|
||||||
|
padding-block: 30px;
|
||||||
|
|
||||||
|
&__slide {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
|
||||||
|
& .card {
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
&: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>
|
||||||
47
storefront/components/home/hero.vue
Normal file
47
storefront/components/home/hero.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<div class="hero" :style="backgroundStyles">
|
||||||
|
<div class="container">
|
||||||
|
<div class="hero__wrapper">
|
||||||
|
<nuxt-img format="webp" densities="x1" src="/images/evibes-big.png" alt="logo" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const img = useImage()
|
||||||
|
|
||||||
|
const backgroundStyles = computed(() => {
|
||||||
|
const imgUrl = img('/images/homeBg.png', { format: 'webp', densities: 'x1 x2' })
|
||||||
|
|
||||||
|
return { backgroundImage: `url('${imgUrl}')` }
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hero {
|
||||||
|
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>
|
||||||
31
storefront/components/skeletons/header/search.vue
Normal file
31
storefront/components/skeletons/header/search.vue
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<el-skeleton class="sk" animated>
|
||||||
|
<template #template>
|
||||||
|
<el-skeleton-item
|
||||||
|
variant="p"
|
||||||
|
class="sk__text"
|
||||||
|
v-for="idx in 5"
|
||||||
|
:key="idx"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-skeleton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sk {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
63
storefront/components/ui/button.vue
Normal file
63
storefront/components/ui/button.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<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 lang="ts">
|
||||||
|
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-block: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
91
storefront/components/ui/checkbox.vue
Normal file
91
storefront/components/ui/checkbox.vue
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<template>
|
||||||
|
<label class="checkbox" :class="{ isFilter }">
|
||||||
|
<input
|
||||||
|
:id="id"
|
||||||
|
class="checkbox__input"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="modelValue"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
<span class="checkbox__block"></span>
|
||||||
|
<span class="checkbox__label">
|
||||||
|
<slot/>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
id: string,
|
||||||
|
modelValue: boolean,
|
||||||
|
isFilter: boolean
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', v: boolean): void
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function onChange(e: Event) {
|
||||||
|
const checked = (e.target as HTMLInputElement).checked;
|
||||||
|
emit('update:modelValue', checked);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.isFilter {
|
||||||
|
& .checkbox__block {
|
||||||
|
border: 2px solid $accent;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .checkbox__label {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__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/components/ui/counter.vue
Normal file
27
storefront/components/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>
|
||||||
146
storefront/components/ui/input.vue
Normal file
146
storefront/components/ui/input.vue
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<Icon v-if="isPasswordVisible === 'password'" name="mdi:eye-off-outline" />
|
||||||
|
<Icon v-else name="mdi:eye-outline" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p v-if="!validate" class="block__error">{{ errorMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const $emit = defineEmits();
|
||||||
|
const props = defineProps<{
|
||||||
|
type: string,
|
||||||
|
placeholder: string,
|
||||||
|
isError?: boolean,
|
||||||
|
error?: string,
|
||||||
|
modelValue?: [string, number],
|
||||||
|
rules?: array,
|
||||||
|
numberOnly: boolean
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isPasswordVisible = ref<string>(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<boolean>(true);
|
||||||
|
const errorMessage = ref<string>('');
|
||||||
|
const onInput = (e: Event) => {
|
||||||
|
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) {
|
||||||
|
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>
|
||||||
141
storefront/components/ui/language-switcher.vue
Normal file
141
storefront/components/ui/language-switcher.vue
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
<template>
|
||||||
|
<div class="switcher" ref="switcherRef">
|
||||||
|
<div
|
||||||
|
@click="setSwitcherVisible(!isSwitcherVisible)"
|
||||||
|
class="switcher__button"
|
||||||
|
:class="[{ active: isSwitcherVisible }]"
|
||||||
|
>
|
||||||
|
<client-only>
|
||||||
|
<nuxt-img
|
||||||
|
format="webp"
|
||||||
|
densities="x1"
|
||||||
|
v-if="currentLocale"
|
||||||
|
:src="currentLocale.flag"
|
||||||
|
:alt="currentLocale.code"
|
||||||
|
/>
|
||||||
|
<skeletons-ui-language-switcher v-else />
|
||||||
|
<template #fallback>
|
||||||
|
<skeletons-ui-language-switcher />
|
||||||
|
</template>
|
||||||
|
</client-only>
|
||||||
|
</div>
|
||||||
|
<client-only>
|
||||||
|
<div
|
||||||
|
class="switcher__menu"
|
||||||
|
:class="[{active: isSwitcherVisible}]"
|
||||||
|
>
|
||||||
|
<div class="switcher__menu-wrapper">
|
||||||
|
<nuxt-img
|
||||||
|
class="switcher__menu-button"
|
||||||
|
v-for="locale of locales"
|
||||||
|
:key="locale.code"
|
||||||
|
format="webp"
|
||||||
|
densities="x1"
|
||||||
|
@click="switchLanguage(locale.code)"
|
||||||
|
:src="locale.flag"
|
||||||
|
:alt="locale.code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</client-only>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {onClickOutside} from "@vueuse/core";
|
||||||
|
import {useLanguageSwitch} from "@/composables/languages/index.js";
|
||||||
|
|
||||||
|
const languageStore = useLanguageStore()
|
||||||
|
|
||||||
|
const locales = computed(() => languageStore.languages)
|
||||||
|
const currentLocale = computed(() => languageStore.currentLocale)
|
||||||
|
|
||||||
|
const isSwitcherVisible = ref<boolean>(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%);
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
transition: grid-template-rows 0.2s ease;
|
||||||
|
|
||||||
|
&-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 8px;
|
||||||
|
background-color: #ddd9ef;
|
||||||
|
transition: 0.1s;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
storefront/components/ui/link.vue
Normal file
36
storefront/components/ui/link.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<div @click="redirect" class="link">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
routePath: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const redirect = () => {
|
||||||
|
if (props.routePath) {
|
||||||
|
router.push({
|
||||||
|
path: props.routePath
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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>
|
||||||
1
storefront/composables/company/index.ts
Normal file
1
storefront/composables/company/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './useCompanyInfo'
|
||||||
|
|
@ -43,7 +43,7 @@ export function useLanguageSwitch() {
|
||||||
|
|
||||||
watch(error, (err) => {
|
watch(error, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('useBrands error:', err)
|
console.error('useLanguageSwitch error:', err)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
3
storefront/composables/products/index.ts
Normal file
3
storefront/composables/products/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './useProducts'
|
||||||
|
export * from './useProductBySlug'
|
||||||
|
export * from './useProductTags'
|
||||||
3
storefront/composables/search/index.ts
Normal file
3
storefront/composables/search/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './useSearch'
|
||||||
|
export * from './useSearchCombined'
|
||||||
|
export * from './useSearchUi'
|
||||||
66
storefront/composables/search/useSearchUi.ts
Normal file
66
storefront/composables/search/useSearchUi.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
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<Record<string, any[]>>((acc, [category, blocks]) => {
|
||||||
|
if (Array.isArray(blocks) && blocks.length > 0) {
|
||||||
|
acc[category] = blocks;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasResults = computed(() => {
|
||||||
|
if (!searchResults.value) return false;
|
||||||
|
|
||||||
|
return Object.values(searchResults.value).some(
|
||||||
|
(blocks) => Array.isArray(blocks) && blocks.length > 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getBlockTitle(category: string) {
|
||||||
|
return category.charAt(0).toUpperCase() + category.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
query.value = '';
|
||||||
|
searchResults.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSearch(value: boolean) {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
88
storefront/config/constants.ts
Normal file
88
storefront/config/constants.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import type {LocaleDefinition} from "~/types";
|
||||||
|
|
||||||
|
// LOCALES
|
||||||
|
|
||||||
|
export const SUPPORTED_LOCALES: LocaleDefinition[] = [
|
||||||
|
{
|
||||||
|
code: 'en-gb',
|
||||||
|
file: 'en-gb.json',
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ar-ar',
|
||||||
|
file: 'ar-ar.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'cs-cz',
|
||||||
|
file: 'cs-cz.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'da-dk',
|
||||||
|
file: 'da-dk.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'de-de',
|
||||||
|
file: 'de-de.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'en-us',
|
||||||
|
file: 'en-us.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'es-es',
|
||||||
|
file: 'es-es.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'fr-fr',
|
||||||
|
file: 'fr-fr.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'it-it',
|
||||||
|
file: 'it-it.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ja-jp',
|
||||||
|
file: 'ja-jp.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'nl-nl',
|
||||||
|
file: 'nl-nl.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'pl-pl',
|
||||||
|
file: 'pl-pl.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'pt-br',
|
||||||
|
file: 'pt-br.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ro-ro',
|
||||||
|
file: 'ro-ro.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ru-ru',
|
||||||
|
file: 'ru-ru.json',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'zh-hans',
|
||||||
|
file: 'zh-hans.json',
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_LOCALE = SUPPORTED_LOCALES.find(locale => locale.default)?.code || 'en-gb';
|
||||||
7
storefront/graphql/fragments/brands.fragment.ts
Normal file
7
storefront/graphql/fragments/brands.fragment.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const BRAND_FRAGMENT = gql`
|
||||||
|
fragment Brand on BrandType {
|
||||||
|
uuid
|
||||||
|
name
|
||||||
|
smallLogo
|
||||||
|
}
|
||||||
|
`
|
||||||
9
storefront/graphql/fragments/categories.fragment.ts
Normal file
9
storefront/graphql/fragments/categories.fragment.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export const CATEGORY_FRAGMENT = gql`
|
||||||
|
fragment Category on CategoryType {
|
||||||
|
name
|
||||||
|
uuid
|
||||||
|
image
|
||||||
|
description
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
`
|
||||||
26
storefront/graphql/fragments/orders.fragment.ts
Normal file
26
storefront/graphql/fragments/orders.fragment.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import {PRODUCT_FRAGMENT} from "~/graphql/fragments/products.fragment";
|
||||||
|
|
||||||
|
export const ORDER_FRAGMENT = gql`
|
||||||
|
fragment Order on OrderType {
|
||||||
|
totalPrice
|
||||||
|
uuid
|
||||||
|
status
|
||||||
|
buyTime
|
||||||
|
humanReadableId
|
||||||
|
orderProducts {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
uuid
|
||||||
|
notifications
|
||||||
|
attributes
|
||||||
|
quantity
|
||||||
|
status
|
||||||
|
product {
|
||||||
|
...Product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${PRODUCT_FRAGMENT}
|
||||||
|
`
|
||||||
51
storefront/graphql/fragments/products.fragment.ts
Normal file
51
storefront/graphql/fragments/products.fragment.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
15
storefront/graphql/fragments/user.fragment.ts
Normal file
15
storefront/graphql/fragments/user.fragment.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
export const USER_FRAGMENT = gql`
|
||||||
|
fragment User on UserType {
|
||||||
|
avatar
|
||||||
|
uuid
|
||||||
|
attributes
|
||||||
|
language
|
||||||
|
email
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
phoneNumber
|
||||||
|
balance {
|
||||||
|
amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
15
storefront/graphql/fragments/wishlist.fragment.ts
Normal file
15
storefront/graphql/fragments/wishlist.fragment.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
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}
|
||||||
|
`
|
||||||
89
storefront/graphql/mutations/auth.ts
Normal file
89
storefront/graphql/mutations/auth.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import {USER_FRAGMENT} from "~/graphql/fragments/user.fragment";
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
67
storefront/graphql/mutations/cart.ts
Normal file
67
storefront/graphql/mutations/cart.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
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}
|
||||||
|
`
|
||||||
20
storefront/graphql/mutations/contact.ts
Normal file
20
storefront/graphql/mutations/contact.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
12
storefront/graphql/mutations/deposit.ts
Normal file
12
storefront/graphql/mutations/deposit.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export const DEPOSIT = gql`
|
||||||
|
mutation deposit(
|
||||||
|
$amount: Number!
|
||||||
|
) {
|
||||||
|
contactUs(
|
||||||
|
amount: $amount,
|
||||||
|
) {
|
||||||
|
error
|
||||||
|
received
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
15
storefront/graphql/mutations/languages.ts
Normal file
15
storefront/graphql/mutations/languages.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
export const SWITCH_LANGUAGE = gql`
|
||||||
|
mutation setlanguage(
|
||||||
|
$uuid: UUID!,
|
||||||
|
$language: String,
|
||||||
|
) {
|
||||||
|
updateUser(
|
||||||
|
uuid: $uuid,
|
||||||
|
language: $language
|
||||||
|
) {
|
||||||
|
user {
|
||||||
|
...User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
32
storefront/graphql/mutations/search.ts
Normal file
32
storefront/graphql/mutations/search.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
export const SEARCH = gql`
|
||||||
|
mutation search(
|
||||||
|
$query: String!
|
||||||
|
) {
|
||||||
|
search(
|
||||||
|
query: $query
|
||||||
|
) {
|
||||||
|
results {
|
||||||
|
brands {
|
||||||
|
uuid
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
uuid
|
||||||
|
}
|
||||||
|
posts {
|
||||||
|
uuid
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
products {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
42
storefront/graphql/mutations/user.ts
Normal file
42
storefront/graphql/mutations/user.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import {USER_FRAGMENT} from "~/graphql/fragments/user.fragment";
|
||||||
|
|
||||||
|
export const ACTIVATE_USER = gql`
|
||||||
|
mutation activateUser(
|
||||||
|
$token: String!,
|
||||||
|
$uid: String!
|
||||||
|
) {
|
||||||
|
activateUser(
|
||||||
|
token: $token,
|
||||||
|
uid: $uid
|
||||||
|
) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const UPDATE_USER = gql`
|
||||||
|
mutation updateUser(
|
||||||
|
$uuid: UUID!,
|
||||||
|
$firstName: String,
|
||||||
|
$lastName: String,
|
||||||
|
$email: String,
|
||||||
|
$phoneNumber: String,
|
||||||
|
$password: String,
|
||||||
|
$confirmPassword: String,
|
||||||
|
) {
|
||||||
|
updateUser(
|
||||||
|
uuid: $uuid,
|
||||||
|
firstName: $firstName,
|
||||||
|
lastName: $lastName,
|
||||||
|
email: $email,
|
||||||
|
phoneNumber: $phoneNumber,
|
||||||
|
password: $password,
|
||||||
|
confirmPassword: $confirmPassword,
|
||||||
|
) {
|
||||||
|
user {
|
||||||
|
...User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${USER_FRAGMENT}
|
||||||
|
`
|
||||||
50
storefront/graphql/mutations/wishlist.ts
Normal file
50
storefront/graphql/mutations/wishlist.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import {WISHLIST_FRAGMENT} from "@/graphql/fragments/wishlist.fragment.js";
|
||||||
|
|
||||||
|
export const ADD_TO_WISHLIST = gql`
|
||||||
|
mutation addToWishlist(
|
||||||
|
$wishlistUuid: String!,
|
||||||
|
$productUuid: String!
|
||||||
|
) {
|
||||||
|
addWishlistProduct(
|
||||||
|
wishlistUuid: $wishlistUuid,
|
||||||
|
productUuid: $productUuid
|
||||||
|
) {
|
||||||
|
wishlist {
|
||||||
|
...Wishlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${WISHLIST_FRAGMENT}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const REMOVE_FROM_WISHLIST = gql`
|
||||||
|
mutation removeFromWishlist(
|
||||||
|
$wishlistUuid: String!,
|
||||||
|
$productUuid: String!
|
||||||
|
) {
|
||||||
|
removeWishlistProduct(
|
||||||
|
wishlistUuid: $wishlistUuid,
|
||||||
|
productUuid: $productUuid
|
||||||
|
) {
|
||||||
|
wishlist {
|
||||||
|
...Wishlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${WISHLIST_FRAGMENT}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const REMOVE_ALL_FROM_WISHLIST = gql`
|
||||||
|
mutation removeAllFromWishlist(
|
||||||
|
$wishlistUuid: String!
|
||||||
|
) {
|
||||||
|
removeAllWishlistProducts(
|
||||||
|
wishlistUuid: $wishlistUuid
|
||||||
|
) {
|
||||||
|
wishlist {
|
||||||
|
...Wishlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${WISHLIST_FRAGMENT}
|
||||||
|
`
|
||||||
27
storefront/graphql/queries/standalone/blog.ts
Normal file
27
storefront/graphql/queries/standalone/blog.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
export const GET_POSTS = gql`
|
||||||
|
query getPosts {
|
||||||
|
posts {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GET_POST_BY_SLUG = gql`
|
||||||
|
query getPostBySlug(
|
||||||
|
$slug: String!
|
||||||
|
) {
|
||||||
|
posts(
|
||||||
|
slug: $slug
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
40
storefront/graphql/queries/standalone/brands.ts
Normal file
40
storefront/graphql/queries/standalone/brands.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {BRAND_FRAGMENT} from "@/graphql/fragments/brands.fragment.js";
|
||||||
|
import {CATEGORY_FRAGMENT} from "@/graphql/fragments/categories.fragment.js";
|
||||||
|
|
||||||
|
export const GET_BRANDS = gql`
|
||||||
|
query getBrands (
|
||||||
|
$brandName: String
|
||||||
|
) {
|
||||||
|
brands(
|
||||||
|
name: $brandName
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Brand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${BRAND_FRAGMENT}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GET_BRAND_BY_UUID = gql`
|
||||||
|
query getBrandbyUuid(
|
||||||
|
$uuid: String!
|
||||||
|
) {
|
||||||
|
brands(
|
||||||
|
uuid: $uuid
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Brand
|
||||||
|
categories {
|
||||||
|
...Category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${BRAND_FRAGMENT}
|
||||||
|
${CATEGORY_FRAGMENT}
|
||||||
|
`
|
||||||
14
storefront/graphql/queries/standalone/company.ts
Normal file
14
storefront/graphql/queries/standalone/company.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const GET_COMPANY_INFO = gql`
|
||||||
|
query getCompanyInfo {
|
||||||
|
parameters {
|
||||||
|
companyAddress
|
||||||
|
companyName
|
||||||
|
companyPhoneNumber
|
||||||
|
emailFrom
|
||||||
|
emailHostUser
|
||||||
|
projectName
|
||||||
|
paymentGatewayMinimum
|
||||||
|
paymentGatewayMaximum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
9
storefront/graphql/queries/standalone/languages.ts
Normal file
9
storefront/graphql/queries/standalone/languages.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export const GET_LANGUAGES = gql`
|
||||||
|
query getLanguages {
|
||||||
|
languages {
|
||||||
|
code
|
||||||
|
flag
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
21
storefront/graphql/queries/standalone/orders.ts
Normal file
21
storefront/graphql/queries/standalone/orders.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {ORDER_FRAGMENT} from "@/graphql/fragments/orders.fragment.js";
|
||||||
|
|
||||||
|
export const GET_ORDERS = gql`
|
||||||
|
query getOrders(
|
||||||
|
$status: String!,
|
||||||
|
$userEmail: String!
|
||||||
|
) {
|
||||||
|
orders(
|
||||||
|
status: $status,
|
||||||
|
orderBy: "-buyTime",
|
||||||
|
userEmail: $userEmail
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${ORDER_FRAGMENT}
|
||||||
|
`
|
||||||
74
storefront/graphql/queries/standalone/products.ts
Normal file
74
storefront/graphql/queries/standalone/products.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import {PRODUCT_FRAGMENT} from "@/graphql/fragments/products.fragment.js";
|
||||||
|
|
||||||
|
export const GET_PRODUCTS = gql`
|
||||||
|
query getProducts(
|
||||||
|
$after: String,
|
||||||
|
$first: Int,
|
||||||
|
$categoriesSlugs: String,
|
||||||
|
$orderBy: String,
|
||||||
|
$minPrice: Decimal,
|
||||||
|
$maxPrice: Decimal,
|
||||||
|
$productName: String
|
||||||
|
) {
|
||||||
|
products(
|
||||||
|
after: $after,
|
||||||
|
first: $first,
|
||||||
|
categoriesSlugs: $categoriesSlugs,
|
||||||
|
orderBy: $orderBy,
|
||||||
|
minPrice: $minPrice,
|
||||||
|
maxPrice: $maxPrice,
|
||||||
|
name: $productName
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
cursor
|
||||||
|
node {
|
||||||
|
...Product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${PRODUCT_FRAGMENT}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GET_PRODUCT_BY_SLUG = gql`
|
||||||
|
query getProductBySlug(
|
||||||
|
$slug: String!
|
||||||
|
) {
|
||||||
|
products(
|
||||||
|
slug: $slug
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${PRODUCT_FRAGMENT}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GET_PRODUCT_TAGS = gql`
|
||||||
|
query getProductTags {
|
||||||
|
productTags {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
uuid
|
||||||
|
name
|
||||||
|
tagName
|
||||||
|
productSet {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${PRODUCT_FRAGMENT}
|
||||||
|
`
|
||||||
14
storefront/graphql/queries/standalone/wishlist.ts
Normal file
14
storefront/graphql/queries/standalone/wishlist.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import {WISHLIST_FRAGMENT} from "@/graphql/fragments/wishlist.fragment.js";
|
||||||
|
|
||||||
|
export const GET_WISHLIST = gql`
|
||||||
|
query getWishlist {
|
||||||
|
wishlists {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Wishlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${WISHLIST_FRAGMENT}
|
||||||
|
`
|
||||||
3
storefront/i18n/locales/ar-ar.json
Normal file
3
storefront/i18n/locales/ar-ar.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/cs-cz.json
Normal file
3
storefront/i18n/locales/cs-cz.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/da-dk.json
Normal file
3
storefront/i18n/locales/da-dk.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
135
storefront/i18n/locales/en-gb.json
Normal file
135
storefront/i18n/locales/en-gb.json
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
{
|
||||||
|
"buttons": {
|
||||||
|
"login": "Login",
|
||||||
|
"register": "Register",
|
||||||
|
"addToCart": "Add To Cart",
|
||||||
|
"send": "Send",
|
||||||
|
"goEmail": "Take me to my inbox",
|
||||||
|
"logout": "Log Out",
|
||||||
|
"buy": "Buy Now",
|
||||||
|
"save": "Save",
|
||||||
|
"sendLink": "Send link",
|
||||||
|
"topUp": "Top up"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"required": "This field is required!",
|
||||||
|
"mail": "Email must be valid!",
|
||||||
|
"compare": "Passwords don't match!",
|
||||||
|
"needLower": "Please include lowercase letter.",
|
||||||
|
"needUpper": "Please include uppercase letter.",
|
||||||
|
"needNumber": "Please include number.",
|
||||||
|
"needMin": "Min. 8 characters",
|
||||||
|
"needSpecial": "Please include a special character: #.?!$%^&*'()_+=:;\"'/>.<,|\\-",
|
||||||
|
"pageNotFound": "Page not found"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"search": "Search",
|
||||||
|
"name": "Name",
|
||||||
|
"firstName": "First name",
|
||||||
|
"lastName": "Last name",
|
||||||
|
"phoneNumber": "Phone number",
|
||||||
|
"email": "Email",
|
||||||
|
"subject": "Subject",
|
||||||
|
"message": "Your message",
|
||||||
|
"password": "Password",
|
||||||
|
"newPassword": "New password",
|
||||||
|
"confirmPassword": "Confirm password",
|
||||||
|
"confirmNewPassword": "Confirm new password"
|
||||||
|
},
|
||||||
|
"checkboxes": {
|
||||||
|
"remember": "Remember me"
|
||||||
|
},
|
||||||
|
"popup": {
|
||||||
|
"errors": {
|
||||||
|
"main": "Error!",
|
||||||
|
"defaultError": "Something went wrong..",
|
||||||
|
"noDataToUpdate": "There is no data to update."
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"login": "Sign in successes",
|
||||||
|
"register": "Account successfully created. Please confirm your Email before Sign In!",
|
||||||
|
"confirmEmail": "Verification E-mail link successfully sent!",
|
||||||
|
"reset": "If specified email exists in our system, we will send a password recovery email!",
|
||||||
|
"newPassword": "You have successfully changed your password!",
|
||||||
|
"contactUs": "Your message was sent successfully!"
|
||||||
|
},
|
||||||
|
"addToCart": "{product} has been added to the cart!",
|
||||||
|
"addToCartLimit": "Total quantity limit is {quantity}!",
|
||||||
|
"failAdd": "Please log in to make a purchase",
|
||||||
|
"activationSuccess": "E-mail successfully verified. Please Sign In!",
|
||||||
|
"successUpdate": "Profile successfully updated!",
|
||||||
|
|
||||||
|
"payment": "Your purchase is being processed! Please stand by",
|
||||||
|
"successCheckout": "Order purchase successful!",
|
||||||
|
"addToWishlist": "{product} has been added to the wishlist!"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"actions": {
|
||||||
|
"wishlist": "Wishlist",
|
||||||
|
"cart": "Cart",
|
||||||
|
"login": "Login",
|
||||||
|
"profile": "Profile"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"empty": "Nothing found"
|
||||||
|
},
|
||||||
|
"catalog": {
|
||||||
|
"title": "Catalog"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"address": "Address: ",
|
||||||
|
"email": "Email: ",
|
||||||
|
"phone": "Phone: "
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"collection": {
|
||||||
|
"title": "Our collection",
|
||||||
|
"newTag": "New",
|
||||||
|
"cheapTag": "Low-budget"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forms": {
|
||||||
|
"login": {
|
||||||
|
"title": "Login",
|
||||||
|
"forgot": "Forgot password?",
|
||||||
|
"register": "Don't have an account?"
|
||||||
|
},
|
||||||
|
"register": {
|
||||||
|
"title": "Register",
|
||||||
|
"login": "Do you have an account?"
|
||||||
|
},
|
||||||
|
"reset": {
|
||||||
|
"title": "Reset password"
|
||||||
|
},
|
||||||
|
"newPassword": {
|
||||||
|
"title": "New password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cards": {
|
||||||
|
"product": {
|
||||||
|
"stock": "In stock: "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"breadcrumbs": {
|
||||||
|
"home": "Home",
|
||||||
|
"catalog": "Catalog"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"title": "Contact us"
|
||||||
|
},
|
||||||
|
"store": {
|
||||||
|
"sorting": "Sort by:",
|
||||||
|
"filters": {
|
||||||
|
"title": "Filters",
|
||||||
|
"apply": "Apply",
|
||||||
|
"reset": "Reset"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"products": "Products",
|
||||||
|
"categories": "Categories",
|
||||||
|
"brands": "Brands",
|
||||||
|
"byRequest": "by request"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/en-us.json
Normal file
3
storefront/i18n/locales/en-us.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/es-es.json
Normal file
3
storefront/i18n/locales/es-es.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/fr-fr.json
Normal file
3
storefront/i18n/locales/fr-fr.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/it-it.json
Normal file
3
storefront/i18n/locales/it-it.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/ja-jp.json
Normal file
3
storefront/i18n/locales/ja-jp.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/nl-nl.json
Normal file
3
storefront/i18n/locales/nl-nl.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/pl-pl.json
Normal file
3
storefront/i18n/locales/pl-pl.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/pt-br.json
Normal file
3
storefront/i18n/locales/pt-br.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/ro-ro.json
Normal file
3
storefront/i18n/locales/ro-ro.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/ru-ru.json
Normal file
3
storefront/i18n/locales/ru-ru.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
3
storefront/i18n/locales/zh-hans.json
Normal file
3
storefront/i18n/locales/zh-hans.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -54,9 +54,7 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
css: [
|
css: [
|
||||||
'./assets/styles/main.scss',
|
'./assets/styles/main.scss',
|
||||||
'./assets/styles/global/fonts.scss',
|
'./assets/styles/global/fonts.scss'
|
||||||
'swiper/css',
|
|
||||||
'swiper/css/effect-fade',
|
|
||||||
],
|
],
|
||||||
alias: {
|
alias: {
|
||||||
'styles': fileURLToPath(new URL("./assets/styles", import.meta.url)),
|
'styles': fileURLToPath(new URL("./assets/styles", import.meta.url)),
|
||||||
|
|
|
||||||
13042
storefront/package-lock.json
generated
Normal file
13042
storefront/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
41
storefront/package.json
Normal file
41
storefront/package.json
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "storefront",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt/icon": "^1.13.0",
|
||||||
|
"@nuxt/image": "^1.10.0",
|
||||||
|
"@nuxtjs/i18n": "^9.5.5",
|
||||||
|
"@pinia/nuxt": "^0.11.1",
|
||||||
|
"@vueuse/core": "^13.3.0",
|
||||||
|
"@vueuse/integrations": "^13.3.0",
|
||||||
|
"@vueuse/nuxt": "^13.3.0",
|
||||||
|
"@vueuse/router": "^13.3.0",
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"graphql-combine-query": "^1.2.4",
|
||||||
|
"graphql-tag": "^2.12.6",
|
||||||
|
"nuxt": "^3.17.5",
|
||||||
|
"nuxt-marquee": "^1.0.4",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
|
"sass": "^1.75.0",
|
||||||
|
"sass-loader": "^14.2.1",
|
||||||
|
"swiper": "^11.2.8",
|
||||||
|
"universal-cookie": "^7.2.2",
|
||||||
|
"vue": "^3.5.16",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@element-plus/nuxt": "^1.1.3",
|
||||||
|
"@nuxtjs/apollo": "^5.0.0-alpha.14",
|
||||||
|
"element-plus": "^2.10.1",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vue-tsc": "^2.2.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
storefront/public/favicon.ico
Normal file
BIN
storefront/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 75 KiB |
BIN
storefront/public/images/evibes-big-simple.png
Normal file
BIN
storefront/public/images/evibes-big-simple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
storefront/public/images/evibes-big.png
Normal file
BIN
storefront/public/images/evibes-big.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
BIN
storefront/public/images/homeBg.png
Normal file
BIN
storefront/public/images/homeBg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
13
storefront/stores/wishlist.ts
Normal file
13
storefront/stores/wishlist.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type {IWishlist} from "~/types";
|
||||||
|
|
||||||
|
export const useWishlistStore = defineStore('wishlist', () => {
|
||||||
|
const wishlist = ref<IWishlist | null>(null);
|
||||||
|
const setWishlist = (payload: IWishlist | null) => {
|
||||||
|
wishlist.value = payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
wishlist,
|
||||||
|
setWishlist
|
||||||
|
};
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue