Features: 1) Add Source Code Pro Light font asset for theme enhancement;

Fixes: None;

Extra: None;
This commit is contained in:
Alexandr SaVBaD Waltz 2025-06-27 01:59:02 +03:00
parent 129ad1a6fa
commit a31ee9c6b1
83 changed files with 15862 additions and 4 deletions

View file

@ -0,0 +1,42 @@
/* ===== SOURCE CODE PRO ===== */
@font-face {
font-family: 'Source Code Pro';
src: url('../../fonts/SourceCodePro/SourceCodePro-Black.ttf');
font-weight: 900;
}
@font-face {
font-family: 'Source Code Pro';
src: url('../../fonts/SourceCodePro/SourceCodePro-ExtraBold.ttf');
font-weight: 800;
}
@font-face {
font-family: 'Source Code Pro';
src: url('../../fonts/SourceCodePro/SourceCodePro-Bold.ttf');
font-weight: 700;
}
@font-face {
font-family: 'Source Code Pro';
src: url('../../fonts/SourceCodePro/SourceCodePro-SemiBold.ttf');
font-weight: 600;
}
@font-face {
font-family: 'Source Code Pro';
src: url('../../fonts/SourceCodePro/SourceCodePro-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: 'Source Code Pro';
src: url('../../fonts/SourceCodePro/SourceCodePro-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: 'Source Code Pro';
src: url('../../fonts/SourceCodePro/SourceCodePro-Light.ttf');
font-weight: 300;
}
@font-face {
font-family: 'Source Code Pro';
src: url('../../fonts/SourceCodePro/SourceCodePro-ExtraLight.ttf');
font-weight: 200;
}

View file

@ -0,0 +1,7 @@
@mixin hover {
@media (hover: hover) and (pointer: fine) {
&:hover {
@content;
}
}
}

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

View file

@ -0,0 +1,5 @@
@use "modules/normalize";
@use "modules/transitions";
@use "global/mixins";
@use "global/variables";
@use "ui/collapse";

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

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

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

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

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

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

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

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

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

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

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

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

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

View file

@ -0,0 +1,31 @@
<template>
<el-skeleton class="sk" animated>
<template #template>
<el-skeleton-item
variant="p"
class="sk__text"
v-for="idx in 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>

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

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

View file

@ -0,0 +1,27 @@
<template>
<div class="counter">
<slot></slot>
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.counter {
position: absolute !important;
top: -10px;
right: -15px;
background-color: $accent;
border-radius: 50%;
width: 20px;
aspect-ratio: 1;
display: grid;
place-items: center;
font-size: 14px;
font-weight: 600;
color: $white;
}
</style>

View file

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

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

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

View file

@ -0,0 +1 @@
export * from './useCompanyInfo'

View file

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

View file

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

View file

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

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

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

View file

@ -0,0 +1,7 @@
export const BRAND_FRAGMENT = gql`
fragment Brand on BrandType {
uuid
name
smallLogo
}
`

View file

@ -0,0 +1,9 @@
export const CATEGORY_FRAGMENT = gql`
fragment Category on CategoryType {
name
uuid
image
description
slug
}
`

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

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

View file

@ -0,0 +1,15 @@
export const USER_FRAGMENT = gql`
fragment User on UserType {
avatar
uuid
attributes
language
email
firstName
lastName
phoneNumber
balance {
amount
}
}
`

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

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

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

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

View file

@ -0,0 +1,12 @@
export const DEPOSIT = gql`
mutation deposit(
$amount: Number!
) {
contactUs(
amount: $amount,
) {
error
received
}
}
`

View file

@ -0,0 +1,15 @@
export const SWITCH_LANGUAGE = gql`
mutation setlanguage(
$uuid: UUID!,
$language: String,
) {
updateUser(
uuid: $uuid,
language: $language
) {
user {
...User
}
}
}
`

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

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

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

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

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

View file

@ -0,0 +1,14 @@
export const GET_COMPANY_INFO = gql`
query getCompanyInfo {
parameters {
companyAddress
companyName
companyPhoneNumber
emailFrom
emailHostUser
projectName
paymentGatewayMinimum
paymentGatewayMaximum
}
}
`

View file

@ -0,0 +1,9 @@
export const GET_LANGUAGES = gql`
query getLanguages {
languages {
code
flag
name
}
}
`

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

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

41
storefront/package.json Normal file
View 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"
}
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -0,0 +1,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
};
})