Merge branch 'storefront' into 'main'

* Features: 1) Add initial storefront configuration including app naming, local storage, and supported locales; 2) Introduce global styles with variables, mixins, and modules; 3) Implement router with locale-based navigation and home page setup; 4) Add Pinia stores for authentication and cart management; 5) Provide GraphQL queries for wishlist, categories, products, orders, languages, company info, and documents; 6) Add GraphQL mutations for authentication, cart, contact, deposit, and wishlist operations; 7) Include SVG assets for eye icons; 8) Add localization file for "en-gb" with buttons, errors, and fields text;

* Merge branch 'main' into storefront

* Features: 1) Implement composables for posts, products, categories, languages, and user deposits with lazy loading and GraphQL integration; 2) Add standalone pages for blog, product, store, and profile with scoped SCSS styling; 3) Add reusable UI components including header, footer, input, button, and textarea; 4) Introduce forms for contact and deposit functionality with validation and localization support; 5) Create GraphQL fragments for users, products, categories, company, orders, languages, and wishlist for efficient data fetching;
This commit is contained in:
Alexandr Waltz 2025-05-28 22:37:39 +10:00
commit 31466aeab4
83 changed files with 2048 additions and 683 deletions

View file

@ -1,12 +1,20 @@
<script setup>
import { RouterView } from 'vue-router'
import {useRefresh} from "@/composables/auth/useRefresh.js";
import {onMounted} from "vue";
import {useRefresh} from "@/composables/auth";
import {useCompanyInfo} from "@/composables/company";
import {useLanguages} from "@/composables/languages/index.js";
import BaseHeader from "@/components/base/base-header.vue";
import BaseFooter from "@/components/base/base-footer.vue";
const { refresh } = useRefresh();
const { getCompanyInfo } = useCompanyInfo();
const { getLanguages } = useLanguages();
onMounted(async () => {
await refresh()
await getCompanyInfo()
await getLanguages()
setInterval(async () => {
await refresh()
@ -16,11 +24,13 @@ onMounted(async () => {
<template>
<main class="main" id="top">
<base-header />
<RouterView v-slot="{ Component }">
<Transition name="opacity" mode="out-in">
<component :is="Component" />
</Transition>
</RouterView>
<base-footer />
</main>
</template>

View file

@ -0,0 +1,19 @@
<template>
<footer class="footer">
<div class="container">
<div class="footer_wrapper">
</div>
</div>
</footer>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.footer {
}
</style>

View file

@ -0,0 +1,26 @@
<template>
<header class="header">
<div class="container">
<div class="header__wrapper">
</div>
</div>
</header>
</template>
<script setup>
import {onMounted} from "vue";
import {useCategories} from "@/composables/categories";
const { categories, loading, getCategories } = useCategories();
onMounted(async () => {
await getCategories()
})
</script>
<style lang="scss" scoped>
.header {
}
</style>

View file

@ -0,0 +1,88 @@
<template>
<form @submit.prevent="handleContactUs()" class="form">
<ui-input
:type="'text'"
:placeholder="t('fields.name')"
:rules="[required]"
v-model="name"
/>
<ui-input
:type="'email'"
:placeholder="t('fields.email')"
:rules="[required]"
v-model="email"
/>
<ui-input
:type="'text'"
:placeholder="t('fields.phoneNumber')"
:rules="[required]"
v-model="phoneNumber"
/>
<ui-input
:type="'text'"
:placeholder="t('fields.subject')"
:rules="[required]"
v-model="subject"
/>
<ui-textarea
:placeholder="t('fields.message')"
:rules="[required]"
v-model="message"
/>
<ui-button
class="form__button"
:isDisabled="!isFormValid"
:isLoading="loading"
>
{{ t('buttons.send') }}
</ui-button>
</form>
</template>
<script setup>
import {useI18n} from "vue-i18n";
import {required} from "@/core/rules/textFieldRules.js";
import {computed, ref} from "vue";
import UiInput from "@/components/ui/ui-input.vue";
import UiButton from "@/components/ui/ui-button.vue";
import UiTextarea from "@/components/ui/ui-textarea.vue";
import {useContactUs} from "@/composables/contact/index.js";
const {t} = useI18n()
const name = ref('')
const email = ref('')
const phoneNumber = ref('')
const subject = ref('')
const message = ref('')
const isFormValid = computed(() => {
return (
required(name.value) === true &&
required(email.value) === true &&
required(phoneNumber.value) === true &&
required(subject.value) === true &&
required(message.value) === true
)
})
const { contactUs, loading } = useContactUs();
async function handleContactUs() {
await contactUs(
name.value,
email.value,
phoneNumber.value,
subject.value,
message.value,
);
}
</script>
<style lang="scss" scoped>
.form {
display: flex;
flex-direction: column;
gap: 20px;
}
</style>

View file

@ -0,0 +1,65 @@
<template>
<form @submit.prevent="handleDeposit()" class="form">
<div class="form__box">
<ui-input
:type="'text'"
:placeholder="''"
v-model="amount"
:numberOnly="true"
/>
<ui-input
:type="'text'"
:placeholder="''"
v-model="amount"
:numberOnly="true"
/>
</div>
<ui-button
class="form__button"
:isDisabled="!isFormValid"
:isLoading="loading"
>
{{ t('buttons.topUp') }}
</ui-button>
</form>
</template>
<script setup>
import UiInput from "@/components/ui/ui-input.vue";
import {computed, ref} from "vue";
import UiButton from "@/components/ui/ui-button.vue";
import {useI18n} from "vue-i18n";
import {useDeposit} from "@/composables/user/useDeposit.js";
const {t} = useI18n()
const amount = ref('')
const isFormValid = computed(() => {
return (
amount.value >= 5 && amount.value <= 500
)
})
const onlyNumbersKeypress = (event) => {
if (!/\d/.test(event.key)) {
event.preventDefault();
}
}
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

@ -1,5 +1,5 @@
<template>
<div class="form">
<form @submit.prevent="handleLogin()" class="form">
<ui-input
:type="'email'"
:placeholder="t('fields.email')"
@ -12,15 +12,19 @@
:rules="[required]"
v-model="password"
/>
<ui-checkbox
v-model="isStayLogin"
>
{{ t('checkboxes.remember') }}
</ui-checkbox>
<ui-button
class="form__button"
:isDisabled="!isFormValid"
:isLoading="loading"
@click="handleLogin()"
>
{{ t('buttons.signIn') }}
</ui-button>
</div>
</form>
</template>
<script setup>
@ -28,13 +32,15 @@ import {useI18n} from "vue-i18n";
import {isEmail, required} from "@/core/rules/textFieldRules.js";
import {computed, ref} from "vue";
import UiInput from "@/components/ui/ui-input.vue";
import {useLogin} from "@/composables/auth/useLogin.js";
import UiButton from "@/components/ui/ui-button.vue";
import UiCheckbox from "@/components/ui/ui-checkbox.vue";
import {useLogin} from "@/composables/auth";
const {t} = useI18n()
const email = ref('')
const password = ref('')
const isStayLogin = ref(false)
const isFormValid = computed(() => {
return (
@ -46,7 +52,7 @@ const isFormValid = computed(() => {
const { login, loading } = useLogin();
async function handleLogin() {
await login(email.value, password.value);
await login(email.value, password.value, isStayLogin.value);
}
</script>

View file

@ -0,0 +1,66 @@
<template>
<form @submit.prevent="handleReset()" class="form">
<ui-input
:type="'password'"
:placeholder="t('fields.newPassword')"
:rules="[isPasswordValid]"
v-model="password"
/>
<ui-input
:type="'password'"
:placeholder="t('fields.confirmNewPassword')"
:rules="[compareStrings]"
v-model="confirmPassword"
/>
<ui-button
class="form__button"
:isDisabled="!isFormValid"
:isLoading="loading"
>
{{ t('buttons.save') }}
</ui-button>
</form>
</template>
<script setup>
import {useI18n} from "vue-i18n";
import {isPasswordValid,} from "@/core/rules/textFieldRules.js";
import {computed, ref} from "vue";
import UiInput from "@/components/ui/ui-input.vue";
import UiButton from "@/components/ui/ui-button.vue";
import {useNewPassword} from "@/composables/auth";
const {t} = useI18n()
const password = ref('')
const confirmPassword = ref('')
const compareStrings = (v) => {
if (v === password.value) return true
return t('errors.compare')
}
const isFormValid = computed(() => {
return (
isPasswordValid(password.value) === true &&
compareStrings(confirmPassword.value) === true
)
})
const { newPassword, loading } = useNewPassword();
async function handleReset() {
await newPassword(
password.value,
confirmPassword.value,
);
}
</script>
<style lang="scss" scoped>
.form {
display: flex;
flex-direction: column;
gap: 20px;
}
</style>

View file

@ -1,5 +1,5 @@
<template>
<div class="form">
<form @submit.prevent="handleRegister()" class="form">
<ui-input
:type="'text'"
:placeholder="t('fields.firstName')"
@ -14,7 +14,7 @@
/>
<ui-input
:type="'text'"
:placeholder="t('fields.phone')"
:placeholder="t('fields.phoneNumber')"
:rules="[required]"
v-model="phoneNumber"
/>
@ -40,11 +40,10 @@
class="form__button"
:isDisabled="!isFormValid"
:isLoading="loading"
@click="handleRegister()"
>
{{ t('buttons.signUp') }}
</ui-button>
</div>
</form>
</template>
<script setup>
@ -53,7 +52,7 @@ import {isEmail, isPasswordValid, required} from "@/core/rules/textFieldRules.js
import {computed, ref} from "vue";
import UiInput from "@/components/ui/ui-input.vue";
import UiButton from "@/components/ui/ui-button.vue";
import {useRegister} from "@/composables/auth/useRegister.js";
import {useRegister} from "@/composables/auth/index.js";
const {t} = useI18n()

View file

@ -0,0 +1,50 @@
<template>
<form @submit.prevent="handleReset()" class="form">
<ui-input
:type="'email'"
:placeholder="t('fields.email')"
:rules="[isEmail]"
v-model="email"
/>
<ui-button
class="form__button"
:isDisabled="!isFormValid"
:isLoading="loading"
>
{{ t('buttons.sendLink') }}
</ui-button>
</form>
</template>
<script setup>
import {useI18n} from "vue-i18n";
import {isEmail} from "@/core/rules/textFieldRules.js";
import {computed, ref} from "vue";
import UiInput from "@/components/ui/ui-input.vue";
import UiButton from "@/components/ui/ui-button.vue";
import {usePasswordReset} from "@/composables/auth";
const {t} = useI18n()
const email = ref('')
const isFormValid = computed(() => {
return (
isEmail(email.value) === true
)
})
const { resetPassword, loading } = usePasswordReset();
async function handleReset() {
await resetPassword(email.value);
}
</script>
<style lang="scss" scoped>
.form {
display: flex;
flex-direction: column;
gap: 20px;
}
</style>

View file

@ -0,0 +1,104 @@
<template>
<form class="form" @submit.prevent="handleUpdate()">
<ui-input
:type="'text'"
:placeholder="t('fields.firstName')"
:rules="[required]"
v-model="firstName"
/>
<ui-input
:type="'text'"
:placeholder="t('fields.lastName')"
:rules="[required]"
v-model="lastName"
/>
<ui-input
:type="'email'"
:placeholder="t('fields.email')"
:rules="[isEmail]"
v-model="email"
/>
<ui-input
:type="'text'"
:placeholder="t('fields.phoneNumber')"
:rules="[required]"
v-model="phoneNumber"
/>
<ui-input
:type="'password'"
:placeholder="t('fields.newPassword')"
:rules="[isPasswordValid]"
v-model="password"
/>
<ui-input
:type="'password'"
:placeholder="t('fields.confirmNewPassword')"
:rules="[compareStrings]"
v-model="confirmPassword"
/>
<ui-button
class="form__button"
:isLoading="loading"
>
{{ t('buttons.save') }}
</ui-button>
</form>
</template>
<script setup>
import {useI18n} from "vue-i18n";
import {isEmail, isPasswordValid, required} from "@/core/rules/textFieldRules.js";
import {computed, ref, watchEffect} from "vue";
import UiInput from "@/components/ui/ui-input.vue";
import UiButton from "@/components/ui/ui-button.vue";
import {useAuthStore} from "@/stores/auth.js";
import {useUserUpdating} from "@/composables/user";
const {t} = useI18n()
const authStore = useAuthStore()
const userFirstName = computed(() => authStore.user?.firstName)
const userLastName = computed(() => authStore.user?.lastName)
const userEmail = computed(() => authStore.user?.email)
const userPhoneNumber = computed(() => authStore.user?.phoneNumber)
const 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

@ -1,5 +1,10 @@
<template>
<button class="button" :disabled="isDisabled" :class="[{active: isLoading}]">
<button
class="button"
:disabled="isDisabled"
:class="[{active: isLoading}]"
type="submit"
>
<ui-loader class="button__loader" v-if="isLoading" />
<slot v-else />
</button>
@ -24,6 +29,8 @@ const props = defineProps({
border: 1px solid $black;
background-color: $white;
padding-block: 5px;
display: grid;
place-items: center;
z-index: 1;
color: $black;
@ -33,15 +40,18 @@ const props = defineProps({
&:hover, &.active {
background-color: $black;
color: $white;
}
&:disabled {
cursor: not-allowed;
background-color: rgba($black, 0.5);
color: $black;
}
&:disabled:hover, &.active {
background-color: rgba($black, 0.5);
color: $black;
}
&__loader {

View file

@ -0,0 +1,67 @@
<template>
<div>
<input
:id="'checkbox' + id"
class="checkbox"
type="checkbox"
:value="modelValue"
@input="onInput"
:checked="modelValue"
>
<label :for="'checkbox' + id" class="checkbox__label">
<slot />
</label>
</div>
</template>
<script setup>
const $emit = defineEmits()
const props = defineProps({
id: [Number, String],
modelValue: Boolean
})
const onInput = (event) => {
$emit('update:modelValue', event.target.checked);
};
</script>
<style lang="scss" scoped>
.checkbox {
display: none;
opacity: 0;
&__label {
color: #2B2B2B;
font-size: 12px;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.12px;
}
}
.checkbox + .checkbox__label::before {
content: '';
display: inline-block;
width: 17px;
height: 17px;
flex-shrink: 0;
flex-grow: 0;
border: 1px solid $black;
margin-right: 10px;
background-repeat: no-repeat;
background-position: center center;
background-size: 50% 50%;
}
.checkbox:checked + .checkbox__label::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2f6b4f' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e");
}
.checkbox + .checkbox__label {
cursor: pointer;
display: inline-flex;
align-items: center;
user-select: none;
}
</style>

View file

@ -6,6 +6,7 @@
:type="isPasswordVisible"
:value="modelValue"
@input="onInput"
@keydown="numberOnly ? onlyNumbersKeydown($event) : null"
class="block__input"
>
<button
@ -31,7 +32,8 @@ const props = defineProps({
isError: Boolean,
error: String,
modelValue: [String, Number],
rules: Array
rules: Array,
numberOnly: Boolean
})
const isPasswordVisible = ref(props.type)
@ -43,9 +45,26 @@ const setPasswordVisible = () => {
isPasswordVisible.value = 'password'
}
const onlyNumbersKeydown = (event) => {
if (!/^\d$/.test(event.key) &&
!['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'Tab', 'Home', 'End'].includes(event.key)) {
event.preventDefault();
}
}
const validate = ref(true)
const errorMessage = ref('')
const onInput = (e) => {
let value = e.target.value;
if (props.numberOnly) {
const newValue = value.replace(/\D/g, '');
if (newValue !== value) {
e.target.value = newValue;
value = newValue;
}
}
let result = true
props.rules?.forEach((rule) => {
@ -87,7 +106,6 @@ const onInput = (e) => {
font-size: 12px;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.14px;
&::placeholder {
color: #2B2B2B;

View file

@ -0,0 +1,90 @@
<template>
<div class="block">
<textarea
:placeholder="placeholder"
:value="modelValue"
@input="onInput"
class="block__textarea"
/>
<p v-if="!validate" class="block__error">{{ errorMessage }}</p>
</div>
</template>
<script setup>
import {ref} from "vue";
const $emit = defineEmits()
const props = defineProps({
placeholder: String,
isError: Boolean,
error: String,
modelValue: [String, Number],
rules: Array
})
const validate = ref(true)
const errorMessage = ref('')
const onInput = (e) => {
let result = true
props.rules?.forEach((rule) => {
result = rule((e.target).value)
if (result !== true) {
errorMessage.value = String(result)
result = false
}
})
validate.value = result
return $emit('update:modelValue', (e.target).value)
}
</script>
<style lang="scss" scoped>
.block {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
position: relative;
&__textarea {
width: 100%;
height: 150px;
resize: none;
padding: 6px 12px;
border: 1px solid $black;
background-color: $white;
color: #1f1f1f;
font-size: 12px;
font-weight: 400;
line-height: 20px;
&::placeholder {
color: #2B2B2B;
}
}
&__error {
color: $error;
font-size: 12px;
font-weight: 500;
animation: fadeInUp 0.3s ease;
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(-50%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
}
}
</style>

View file

@ -0,0 +1,6 @@
export * from './useLogin';
export * from './useLogout';
export * from './useNewPassword';
export * from './usePasswordReset';
export * from './useRefresh';
export * from './useRegister';

View file

@ -5,25 +5,33 @@ import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {useAuthStore} from "@/stores/auth.js";
import translations from "@/core/helpers/translations.js";
import {LOCALE_STORAGE_REFRESH_KEY} from "@/config/index.js";
import { useAuthOrder } from './useAuthOrder';
import { useAuthWishlist } from './useAuthWishlist';
import {
DEFAULT_LOCALE,
LOCALE_STORAGE_LOCALE_KEY,
LOCALE_STORAGE_REFRESH_KEY,
LOCALE_STORAGE_STAY_LOGIN_KEY
} from "@/config/index.js";
import {useRoute, useRouter} from "vue-router";
import {usePendingOrder} from "@/composables/orders";
import {useWishlist} from "@/composables/wishlist";
export function useLogin() {
const loading = ref(false);
const userData = ref(null);
const router = useRouter();
const route = useRoute();
const authStore = useAuthStore()
const {t} = useI18n();
const { mutate: loginMutation } = useMutation(LOGIN);
const { getPendingOrder } = useAuthOrder();
const { getWishlist } = useAuthWishlist();
const { getPendingOrder } = usePendingOrder();
const { getWishlist } = useWishlist();
const loading = ref(false);
async function login(
email,
password
password,
isStayLogin
) {
loading.value = true;
@ -33,6 +41,10 @@ export function useLogin() {
password
});
if (isStayLogin) {
localStorage.setItem(LOCALE_STORAGE_STAY_LOGIN_KEY, 'remember')
}
if (response.data?.obtainJwtToken) {
authStore.setUser({
user: response.data.obtainJwtToken.user,
@ -42,26 +54,33 @@ export function useLogin() {
localStorage.setItem(LOCALE_STORAGE_REFRESH_KEY, response.data.obtainJwtToken.refreshToken)
ElNotification({
message: t('popup.login.text'),
message: t('popup.success.login'),
type: 'success'
})
await router.push({
name: 'home',
params: {
locale: localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE
}
})
if (response.data.obtainJwtToken.user.language !== translations.currentLocale) {
translations.switchLanguage(response.data.obtainJwtToken.user.language)
translations.switchLanguage(response.data.obtainJwtToken.user.language, router, route)
}
await getPendingOrder(response.data.obtainJwtToken.user.email);
await getWishlist();
}
} catch (error) {
console.error("Login error:", error);
console.error("useLogin error:", error);
const errorMessage = error.graphQLErrors?.[0]?.message ||
error.message ||
t('popup.genericError');
t('popup.errors.defaultError');
ElNotification({
title: t('popup.error'),
title: t('popup.errors.main'),
message: errorMessage,
type: 'error'
});
@ -72,7 +91,6 @@ export function useLogin() {
return {
login,
loading,
userData
loading
};
}

View file

@ -0,0 +1,34 @@
import {useAuthStore} from "@/stores/auth.js";
import {
DEFAULT_LOCALE,
LOCALE_STORAGE_LOCALE_KEY,
LOCALE_STORAGE_REFRESH_KEY,
LOCALE_STORAGE_STAY_LOGIN_KEY
} from "@/config/index.js";
import {useRouter} from "vue-router";
export function useLogout() {
const authStore = useAuthStore()
const router = useRouter()
async function logout() {
authStore.setUser({
user: null,
accessToken: null
})
localStorage.removeItem(LOCALE_STORAGE_REFRESH_KEY)
localStorage.removeItem(LOCALE_STORAGE_STAY_LOGIN_KEY)
await router.push({
name: 'home',
params: {
locale: localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE
}
})
}
return {
logout
};
}

View file

@ -0,0 +1,73 @@
import {useMutation} from "@vue/apollo-composable";
import {NEW_PASSWORD} from "@/graphql/mutations/auth.js";
import {computed, ref} from "vue";
import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {useRoute, useRouter} from "vue-router";
import {DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY} from "@/config/index.js";
export function useNewPassword() {
const {t} = useI18n();
const route = useRoute();
const router = useRouter();
const { mutate: newPasswordMutation } = useMutation(NEW_PASSWORD);
const token = computed(() =>
route.query.token ? (route.query.token) : undefined,
);
const uid = computed(() =>
route.query.uid ? (route.query.uid) : undefined,
);
const loading = ref(false);
async function newPassword(
password,
confirmPassword
) {
loading.value = true;
try {
const response = await newPasswordMutation({
password,
confirmPassword,
token: token.value,
uid: uid.value
});
if (response.data?.confirmResetPassword.success) {
ElNotification({
message: t('popup.success.newPassword'),
type: 'success'
})
await router.push({
name: 'home',
params: {
locale: localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE
}
})
}
} catch (error) {
console.error("useNewPassword error:", error);
const errorMessage = error.graphQLErrors?.[0]?.message ||
error.message ||
t('popup.errors.defaultError');
ElNotification({
title: t('popup.errors.main'),
message: errorMessage,
type: 'error'
});
} finally {
loading.value = false;
}
}
return {
newPassword,
loading
};
}

View file

@ -0,0 +1,51 @@
import {useMutation} from "@vue/apollo-composable";
import {RESET_PASSWORD} from "@/graphql/mutations/auth.js";
import {ref} from "vue";
import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
export function usePasswordReset() {
const {t} = useI18n();
const { mutate: resetPasswordMutation } = useMutation(RESET_PASSWORD);
const loading = ref(false);
async function resetPassword(
email
) {
loading.value = true;
try {
const response = await resetPasswordMutation({
email
});
if (response.data?.resetPassword.success) {
ElNotification({
message: t('popup.success.reset'),
type: 'success'
})
}
} catch (error) {
console.error("usePasswordReset error:", error);
const errorMessage = error.graphQLErrors?.[0]?.message ||
error.message ||
t('popup.errors.defaultError');
ElNotification({
title: t('popup.errors.main'),
message: errorMessage,
type: 'error'
});
} finally {
loading.value = false;
}
}
return {
resetPassword,
loading
};
}

View file

@ -4,16 +4,13 @@ import {computed, ref} from "vue";
import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {useAuthStore} from "@/stores/auth.js";
import { useAuthOrder } from './useAuthOrder';
import { useAuthWishlist } from './useAuthWishlist';
import {DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY, LOCALE_STORAGE_REFRESH_KEY} from "@/config/index.js";
import {LOCALE_STORAGE_REFRESH_KEY} from "@/config/index.js";
import {useRoute, useRouter} from "vue-router";
import translations from "@/core/helpers/translations.js";
import {usePendingOrder} from "@/composables/orders";
import {useWishlist} from "@/composables/wishlist";
export function useRefresh() {
const loading = ref(false);
const userData = ref(null);
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
@ -21,15 +18,15 @@ export function useRefresh() {
const { mutate: refreshMutation } = useMutation(REFRESH);
const { getPendingOrder } = useAuthOrder();
const { getWishlist } = useAuthWishlist();
const { getPendingOrder } = usePendingOrder();
const { getWishlist } = useWishlist();
const loading = ref(false);
async function refresh() {
loading.value = true;
const refreshToken = computed(() => {
return localStorage.getItem(LOCALE_STORAGE_REFRESH_KEY)
})
const refreshToken = computed(() => localStorage.getItem(LOCALE_STORAGE_REFRESH_KEY))
if (!refreshToken.value) return
@ -45,13 +42,7 @@ export function useRefresh() {
})
if (response.data.refreshJwtToken.user.language !== translations.currentLocale) {
translations.switchLanguage(response.data.refreshJwtToken.user.language)
await router.push({
name: route.name,
params: {
locale: localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE
}
})
translations.switchLanguage(response.data.refreshJwtToken.user.language, router, route)
}
localStorage.setItem(LOCALE_STORAGE_REFRESH_KEY, response.data.refreshJwtToken.refreshToken)
@ -60,14 +51,14 @@ export function useRefresh() {
await getWishlist();
}
} catch (error) {
console.error("Refresh error:", error);
console.error("useRefresh error:", error);
const errorMessage = error.graphQLErrors?.[0]?.message ||
error.message ||
t('popup.genericError');
t('popup.errors.defaultError');
ElNotification({
title: t('popup.error'),
title: t('popup.errors.main'),
message: errorMessage,
type: 'error'
});
@ -78,7 +69,6 @@ export function useRefresh() {
return {
refresh,
loading,
userData
loading
};
}

View file

@ -3,18 +3,17 @@ import {REGISTER} from "@/graphql/mutations/auth.js";
import {h, ref} from "vue";
import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {useMailClient} from "@/composables/auth/useMainClient.js";
import {useMailClient} from "@/composables/utils";
export function useRegister() {
const loading = ref(false);
const mailClient = ref(null)
const {t} = useI18n();
const { mutate: registerMutation } = useMutation(REGISTER);
const { mailClientUrl, detectMailClient, openMailClient } = useMailClient();
const loading = ref(false);
async function register(
firstName,
lastName,
@ -39,9 +38,8 @@ export function useRegister() {
detectMailClient(email);
ElNotification({
title: t('popup.register.title'),
message: h('div', [
h('p', t('popup.register.text')),
h('p', t('popup.success.register')),
mailClientUrl.value ? h(
'button',
{
@ -64,14 +62,14 @@ export function useRegister() {
})
}
} catch (error) {
console.error("Register error:", error);
console.error("useRegister error:", error);
const errorMessage = error.graphQLErrors?.[0]?.message ||
error.message ||
t('popup.genericError');
t('popup.errors.defaultError');
ElNotification({
title: t('popup.error'),
title: t('popup.errors.main'),
message: errorMessage,
type: 'error'
});

View file

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

View file

@ -0,0 +1,24 @@
import { useLazyQuery } from "@vue/apollo-composable";
import {GET_POST_BY_SLUG} from "@/graphql/queries/blog.js";
import {computed} from "vue";
export function usePostbySlug() {
const { result, loading, error, load } = useLazyQuery(GET_POST_BY_SLUG);
const post = computed(() => result.value?.posts.edges[0].node ?? []);
if (error.value) {
console.error("usePostbySlug error:", error.value);
}
const getPost = (slug) => {
return load(null, { slug });
};
return {
post,
loading,
error,
getPost
};
}

View file

@ -0,0 +1,20 @@
import { useLazyQuery } from "@vue/apollo-composable";
import { GET_POSTS } from "@/graphql/queries/blog.js";
import {computed} from "vue";
export function usePosts() {
const { result, loading, error, load } = useLazyQuery(GET_POSTS);
const posts = computed(() => result.value?.posts.edges ?? []);
if (error.value) {
console.error("usePosts error:", error.value);
}
return {
posts,
loading,
error,
getPosts: load
};
}

View file

@ -0,0 +1,2 @@
export * from './useCategories'
export * from './useCategorybySlug'

View file

@ -0,0 +1,20 @@
import { useLazyQuery } from "@vue/apollo-composable";
import {computed} from "vue";
import {GET_CATEGORIES} from "@/graphql/queries/categories.js";
export function useCategories() {
const { result, loading, error, load } = useLazyQuery(GET_CATEGORIES);
const categories = computed(() => result.value?.categories.edges ?? []);
if (error.value) {
console.error("useCategories error:", error.value);
}
return {
categories,
loading,
error,
getCategories: load
};
}

View file

@ -0,0 +1,24 @@
import { useLazyQuery } from "@vue/apollo-composable";
import {computed} from "vue";
import {GET_CATEGORY_BY_SLUG} from "@/graphql/queries/categories.js";
export function usePostbySlug() {
const { result, loading, error, load } = useLazyQuery(GET_CATEGORY_BY_SLUG);
const category = computed(() => result.value?.categories.edges[0].node ?? []);
if (error.value) {
console.error("usePostbySlug error:", error.value);
}
const getCategory = (slug) => {
return load(null, { slug });
};
return {
category,
loading,
error,
getCategory
};
}

View file

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

View file

@ -0,0 +1,26 @@
import {useLazyQuery} from "@vue/apollo-composable";
import {GET_COMPANY_INFO} from "@/graphql/queries/company.js";
import {useCompanyStore} from "@/stores/company.js";
import {watchEffect} from "vue";
export function useCompanyInfo() {
const companyStore = useCompanyStore()
const { result, loading, error, load } = useLazyQuery(GET_COMPANY_INFO);
if (error.value) {
console.error("useCompanyInfo error:", error.value);
}
watchEffect(() => {
if (result.value?.parameters) {
companyStore.setCompanyInfo(result.value.parameters);
}
});
return {
loading,
error,
getCompanyInfo: load
};
}

View file

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

View file

@ -0,0 +1,59 @@
import {useMutation} from "@vue/apollo-composable";
import {ref} from "vue";
import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {CONTACT_US} from "@/graphql/mutations/contact.js";
export function useContactUs() {
const {t} = useI18n();
const { mutate: contactUsMutation } = useMutation(CONTACT_US);
const loading = ref(false);
async function contactUs(
name,
email,
phoneNumber,
subject,
message
) {
loading.value = true;
try {
const response = await contactUsMutation({
name,
email,
phoneNumber,
subject,
message
});
if (response.data?.contactUs.received) {
ElNotification({
message: t('popup.success.contactUs'),
type: 'success'
})
}
} catch (error) {
console.error("useContactUs error:", error);
const errorMessage = error.graphQLErrors?.[0]?.message ||
error.message ||
t('popup.errors.defaultError');
ElNotification({
title: t('popup.errors.main'),
message: errorMessage,
type: 'error'
});
} finally {
loading.value = false;
}
}
return {
contactUs,
loading
};
}

View file

@ -0,0 +1,2 @@
export * from './useLanguageSwitch.js'
export * from './useLanguages'

View file

@ -0,0 +1,61 @@
import {useMutation} from "@vue/apollo-composable";
import {computed, ref} from "vue";
import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {useAuthStore} from "@/stores/auth.js";
import translations from "@/core/helpers/translations.js";
import {SWITCH_LANGUAGE} from "@/graphql/mutations/languages.js";
export function useLanguageSwitch() {
const authStore = useAuthStore()
const {t} = useI18n();
const { mutate: languageSwitchMutation } = useMutation(SWITCH_LANGUAGE);
const accessToken = computed(() => authStore.accessToken)
const userUuid = computed(() => authStore.user?.uuid)
const loading = ref(false);
async function switchLanguage(
locale
) {
loading.value = true;
try {
translations.switchLanguage(locale)
if (accessToken.value) {
const response = await languageSwitchMutation(
userUuid.value,
locale
);
if (response.data?.updateUser) {
authStore.setUser({
user: response.data.updateUser.user,
accessToken: accessToken.value
})
}
}
} catch (error) {
console.error("useLanguageSet error:", error);
const errorMessage = error.graphQLErrors?.[0]?.message ||
error.message ||
t('popup.errors.defaultError');
ElNotification({
title: t('popup.errors.main'),
message: errorMessage,
type: 'error'
});
} finally {
loading.value = false;
}
}
return {
switchLanguage,
loading
};
}

View file

@ -0,0 +1,33 @@
import { useLazyQuery } from "@vue/apollo-composable";
import {watchEffect} from "vue";
import {GET_LANGUAGES} from "@/graphql/queries/languages.js";
import {useLanguageStore} from "@/stores/languages.js";
import {SUPPORTED_LOCALES} from "@/config/index.js";
export function useLanguages() {
const languageStore = useLanguageStore()
const { result, loading, error, load } = useLazyQuery(GET_LANGUAGES);
if (error.value) {
console.error("useLanguages error:", error.value);
}
watchEffect(() => {
if (result.value?.languages) {
languageStore.setLanguages(
result.value.languages.filter((locale) =>
SUPPORTED_LOCALES.some(supportedLocale =>
supportedLocale.code === locale.code
)
)
);
}
});
return {
loading,
error,
getLanguages: load
};
}

View file

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

View file

@ -2,7 +2,7 @@ import {useMutation} from "@vue/apollo-composable";
import {GET_ORDERS} from "@/graphql/queries/orders.js";
import {useCartStore} from "@/stores/cart.js";
export function useAuthOrder() {
export function usePendingOrder() {
const cartStore = useCartStore()
const { mutate: pendingOrderMutation } = useMutation(GET_ORDERS);

View file

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

View file

@ -0,0 +1,24 @@
import { useLazyQuery } from "@vue/apollo-composable";
import {computed} from "vue";
import {GET_PRODUCT_BY_SLUG} from "@/graphql/queries/products.js";
export function useProductbySlug() {
const { result, loading, error, load } = useLazyQuery(GET_PRODUCT_BY_SLUG);
const product = computed(() => result.value?.products.edges[0].node ?? []);
if (error.value) {
console.error("useProductbySlug error:", error.value);
}
const getProduct = (slug) => {
return load(null, { slug });
};
return {
product,
loading,
error,
getProduct
};
}

View file

@ -0,0 +1,53 @@
import { ref } from 'vue';
import { useQuery } from '@vue/apollo-composable';
import {GET_PRODUCTS} from "@/graphql/queries/products.js";
export function useProducts() {
const products = ref([]);
const pageInfo = ref([]);
const loading = ref(false);
const getProducts = async (params = {}) => {
loading.value = true;
const defaults = {
first: 12
};
const variables = {};
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
variables[key] = value;
}
});
Object.entries(defaults).forEach(([key, value]) => {
if (!(key in variables)) {
variables[key] = value;
}
});
try {
const { onResult } = useQuery(GET_PRODUCTS, variables);
onResult(result => {
if (result.data && result.data.products) {
products.value = result.data.products.edges;
pageInfo.value = result.data.products.pageInfo;
}
loading.value = false;
});
} catch (error) {
console.error('useProducts error:', error);
loading.value = false;
}
};
return {
products,
pageInfo,
loading,
getProducts
};
}

View file

@ -0,0 +1,3 @@
export * from './useUserUpdating';
export * from './useUserActivation';
export * from '../languages/useLanguageSwitch.js';

View file

@ -0,0 +1,48 @@
import {useMutation} from "@vue/apollo-composable";
import {ref} from "vue";
import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {DEPOSIT} from "@/graphql/mutations/deposit.js";
export function useDeposit() {
const {t} = useI18n();
const { mutate: depositMutation } = useMutation(DEPOSIT);
const loading = ref(false);
async function deposit(
amount
) {
loading.value = true;
try {
const response = await depositMutation(
amount
);
if (response.data?.deposit) {
window.open(response.data.deposit.transaction.process.url)
}
} catch (error) {
console.error("useDeposit error:", error);
const errorMessage = error.graphQLErrors?.[0]?.message ||
error.message ||
t('popup.errors.defaultError');
ElNotification({
title: t('popup.errors.main'),
message: errorMessage,
type: 'error'
});
} finally {
loading.value = false;
}
}
return {
deposit,
loading
};
}

View file

@ -0,0 +1,59 @@
import {useMutation} from "@vue/apollo-composable";
import {computed, ref} from "vue";
import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {useRoute} from "vue-router";
import {ACTIVATE_USER} from "@/graphql/mutations/user.js";
export function useUserActivation() {
const {t} = useI18n();
const route = useRoute();
const { mutate: userActivationMutation } = useMutation(ACTIVATE_USER);
const token = computed(() =>
route.query.token ? (route.query.token) : undefined,
);
const uid = computed(() =>
route.query.uid ? (route.query.uid) : undefined,
);
const loading = ref(false);
async function activateUser() {
loading.value = true;
try {
const response = await userActivationMutation({
token: token.value,
uid: uid.value
});
if (response.data?.activateUser) {
ElNotification({
message: t("popup.activationSuccess"),
type: "success"
});
}
} catch (error) {
console.error("useUserActivation error:", error);
const errorMessage = error.graphQLErrors?.[0]?.message ||
error.message ||
t('popup.errors.defaultError');
ElNotification({
title: t('popup.errors.main'),
message: errorMessage,
type: 'error'
});
} finally {
loading.value = false;
}
}
return {
activateUser,
loading
};
}

View file

@ -0,0 +1,120 @@
import {useMutation} from "@vue/apollo-composable";
import {computed, ref} from "vue";
import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {useAuthStore} from "@/stores/auth.js";
import translations from "@/core/helpers/translations.js";
import {useRoute, useRouter} from "vue-router";
import {useLogout} from "@/composables/auth";
import {UPDATE_USER} from "@/graphql/mutations/user.js";
export function useUserUpdating() {
const router = useRouter();
const route = useRoute();
const authStore = useAuthStore()
const {t} = useI18n();
const { mutate: userUpdatingMutation } = useMutation(UPDATE_USER);
const { logout } = useLogout();
const accessToken = computed(() => authStore.accessToken)
const userUuid = computed(() => authStore.user?.uuid)
const userEmail = computed(() => authStore.user?.email)
const loading = ref(false);
async function updateUser(
firstName,
lastName,
email,
phoneNumber,
password,
confirmPassword
) {
loading.value = true;
try {
const fields = {
uuid: userUuid.value,
firstName,
lastName,
email,
phoneNumber,
password,
confirmPassword
};
const params = Object.fromEntries(
Object.entries(fields).filter(([_, value]) =>
value !== undefined && value !== null && value !== ''
)
);
// if (('password' in params && !('passwordConfirm' in params)) ||
// (!('password' in params) && 'passwordConfirm' in params)) {
// ElNotification({
// title: t('popup.errors.main'),
// message: t('popup.errors.noDataToUpdate'),
// type: 'error'
// });
// }
if (Object.keys(params).length === 0) {
ElNotification({
title: t('popup.errors.main'),
message: t('popup.errors.noDataToUpdate'),
type: 'error'
});
}
const response = await userUpdatingMutation(
params
);
if (response.data?.updateUser) {
if (userEmail.value !== email) {
await logout()
ElNotification({
message: t("popup.success.confirmEmail"),
type: "success"
});
} else {
authStore.setUser({
user: response.data.updateUser.user,
accessToken: accessToken.value
})
ElNotification({
message: t("popup.successUpdate"),
type: "success"
});
if (response.data.updateUser.user.language !== translations.currentLocale) {
translations.switchLanguage(response.data.updateUser.user.language, router, route)
}
}
}
} catch (error) {
console.error("useUserUpdating error:", error);
const errorMessage = error.graphQLErrors?.[0]?.message ||
error.message ||
t('popup.errors.defaultError');
ElNotification({
title: t('popup.errors.main'),
message: errorMessage,
type: 'error'
});
} finally {
loading.value = false;
}
}
return {
updateUser,
loading
};
}

View file

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

View file

@ -6,8 +6,12 @@ export function useMailClient() {
const mailClients = {
'gmail.com': 'https://mail.google.com/',
'outlook.com': 'https://outlook.live.com/',
'icloud.com': 'https://www.icloud.com/',
'yahoo.com': 'https://mail.yahoo.com/'
'icloud.com': 'https://www.icloud.com/mail/',
'yahoo.com': 'https://mail.yahoo.com/',
'mail.ru': 'https://e.mail.ru/inbox/',
'yandex.ru': 'https://mail.yandex.ru/',
'proton.me': 'https://account.proton.me/mail',
'fastmail.com': 'https://fastmail.com/'
};
function detectMailClient(email) {

View file

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

View file

@ -2,7 +2,7 @@ import {useMutation} from "@vue/apollo-composable";
import {GET_WISHLIST} from "@/graphql/queries/wishlist.js";
import {useWishlistStore} from "@/stores/wishlist.js";
export function useAuthWishlist() {
export function useWishlist() {
const wishlistStore = useWishlistStore()
const { mutate: wishlistMutation } = useMutation(GET_WISHLIST);

View file

@ -2,7 +2,7 @@
export const APP_NAME = import.meta.env.EVIBES_PROJECT_NAME
export const APP_NAME_KEY = import.meta.env.EVIBES_PROJECT_NAME.toLowerCase()
export const APP_NAME_KEY = APP_NAME.toLowerCase()
@ -23,4 +23,6 @@ export const DEFAULT_LOCALE = SUPPORTED_LOCALES.find(locale => locale.default)?.
export const LOCALE_STORAGE_LOCALE_KEY = `${APP_NAME_KEY}-user-locale`;
export const LOCALE_STORAGE_REFRESH_KEY = `${APP_NAME_KEY}-refresh`;
export const LOCALE_STORAGE_REFRESH_KEY = `${APP_NAME_KEY}-refresh`;
export const LOCALE_STORAGE_STAY_LOGIN_KEY = `${APP_NAME_KEY}-remember`;

View file

@ -1,7 +1,7 @@
import i18n from '@/core/plugins/i18n.config';
import {DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY, SUPPORTED_LOCALES} from "@/config/index.js";
const translation = {
const translations = {
get currentLocale() {
return i18n.global.locale.value
},
@ -10,12 +10,28 @@ const translation = {
i18n.global.locale.value = newLocale
},
switchLanguage(newLocale) {
translation.currentLocale = newLocale
switchLanguage(newLocale, router = null, route = null) {
translations.currentLocale = newLocale
document.querySelector('html').setAttribute('lang', newLocale)
localStorage.setItem(LOCALE_STORAGE_LOCALE_KEY, newLocale)
if (router && route) {
const newRoute = {
...route,
params: {
...route.params,
locale: newLocale
}
};
router.push(newRoute).catch(err => {
if (err.name !== 'NavigationDuplicated') {
console.error('Navigation error:', err);
}
});
}
},
isLocaleSupported(locale) {
@ -39,7 +55,7 @@ const translation = {
getPersistedLocale() {
const persistedLocale = localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY)
if (translation.isLocaleSupported(persistedLocale)) {
if (translations.isLocaleSupported(persistedLocale)) {
return persistedLocale
} else {
return null
@ -47,18 +63,18 @@ const translation = {
},
guessDefaultLocale() {
const userPersistedLocale = translation.getPersistedLocale()
const userPersistedLocale = translations.getPersistedLocale()
if (userPersistedLocale) {
return userPersistedLocale
}
const userPreferredLocale = translation.getUserLocale()
const userPreferredLocale = translations.getUserLocale()
if (translation.isLocaleSupported(userPreferredLocale.locale)) {
if (translations.isLocaleSupported(userPreferredLocale.locale)) {
return userPreferredLocale.locale
}
if (translation.isLocaleSupported(userPreferredLocale.localeNoRegion)) {
if (translations.isLocaleSupported(userPreferredLocale.localeNoRegion)) {
return userPreferredLocale.localeNoRegion
}
@ -68,11 +84,11 @@ const translation = {
async routeMiddleware(to, _from, next) {
const paramLocale = to.params.locale
if (!translation.isLocaleSupported(paramLocale)) {
return next(translation.guessDefaultLocale())
if (!translations.isLocaleSupported(paramLocale)) {
return next(translations.guessDefaultLocale())
}
await translation.switchLanguage(paramLocale)
await translations.switchLanguage(paramLocale)
return next()
},
@ -81,11 +97,11 @@ const translation = {
return {
...to,
params: {
locale: translation.currentLocale,
locale: translations.currentLocale,
...to.params
}
}
}
}
export default translation
export default translations

View file

@ -0,0 +1,11 @@
import gql from 'graphql-tag'
export const CATEGORY_FRAGMENT = gql`
fragment Category on CategoryType {
name
uuid
image
description
slug
}
`

View file

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

View file

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

View file

@ -0,0 +1,28 @@
import gql from 'graphql-tag'
import {PRODUCT_FRAGMENT} from "@/graphql/fragments/products.fragment.js";
export const ORDER_FRAGMENT = gql`
fragment Order on OrderType {
totalPrice
uuid
status
buyTime
totalPrice
humanReadableId
orderProducts {
edges {
node {
uuid
notifications
attributes
quantity
status
product {
...Product
}
}
}
}
}
${PRODUCT_FRAGMENT}
`

View file

@ -0,0 +1,37 @@
import gql from 'graphql-tag'
export const PRODUCT_FRAGMENT = gql`
fragment Product on ProductType {
uuid
name
price
quantity
slug
category {
name
}
images {
edges {
node {
image
}
}
}
attributeGroups {
edges {
node {
name
uuid
attributes {
name
uuid
values {
value
uuid
}
}
}
}
}
}
`

View file

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

View file

@ -0,0 +1,16 @@
import gql from 'graphql-tag'
import {PRODUCT_FRAGMENT} from "@/graphql/fragments/products.fragment.js";
export const WISHLIST_FRAGMENT = gql`
fragment Wishlist on WishlistType {
uuid
products {
edges {
node {
...Product
}
}
}
}
${PRODUCT_FRAGMENT}
`

View file

@ -1,4 +1,5 @@
import gql from 'graphql-tag'
import {USER_FRAGMENT} from "@/graphql/fragments/user.fragment.js";
export const REGISTER = gql`
mutation register(
@ -34,20 +35,11 @@ export const LOGIN = gql`
accessToken
refreshToken
user {
avatar
uuid
attributes
language
email
firstName
lastName
phoneNumber
balance {
amount
}
...User
}
}
}
${USER_FRAGMENT}
`
export const REFRESH = gql`
@ -60,68 +52,11 @@ export const REFRESH = gql`
accessToken
refreshToken
user {
avatar
uuid
attributes
language
email
firstName
lastName
phoneNumber
balance {
amount
}
}
}
}
`
export const ACTIVATE_USER = gql`
mutation activateUser(
$token: String!,
$uid: String!
) {
activateUser(
token: $token,
uid: $uid
) {
success
}
}
`
export const UPDATE_USER = gql`
mutation updateUser(
$firstName: String,
$lastName: String,
$email: String,
$phoneNumber: String,
$password: String,
$confirmPassword: String,
) {
updateUser(
firstName: $firstName,
lastName: $lastName,
email: $email,
phoneNumber: $phoneNumber,
password: $password,
confirmPassword: $confirmPassword,
) {
user {
avatar
uuid
attributes
language
email
firstName
lastName
phoneNumber
balance {
amount
}
...User
}
}
}
${USER_FRAGMENT}
`
export const RESET_PASSWORD = gql`
@ -136,7 +71,7 @@ export const RESET_PASSWORD = gql`
}
`
export const CONFIRM_RESET_PASSWORD = gql`
export const NEW_PASSWORD = gql`
mutation confirmResetPassword(
$password: String!,
$confirmPassword: String!,

View file

@ -1,4 +1,5 @@
import gql from 'graphql-tag'
import {ORDER_FRAGMENT} from "@/graphql/fragments/orders.fragment.js";
export const ADD_TO_CART = gql`
mutation addToCart(
@ -10,61 +11,11 @@ export const ADD_TO_CART = gql`
productUuid: $productUuid
) {
order {
status
uuid
totalPrice
orderProducts {
edges {
node {
uuid
notifications
attributes
quantity
status
product {
uuid
price
name
description
quantity
slug
category {
name
}
images {
edges {
node {
uuid
image
}
}
}
category {
name
}
attributeGroups {
edges {
node {
name
uuid
attributes {
name
uuid
values {
value
uuid
}
}
}
}
}
}
}
}
}
...Order
}
}
}
${ORDER_FRAGMENT}
`
export const REMOVE_FROM_CART = gql`
@ -77,61 +28,11 @@ export const REMOVE_FROM_CART = gql`
productUuid: $productUuid
) {
order {
status
uuid
totalPrice
orderProducts {
edges {
node {
uuid
notifications
attributes
quantity
status
product {
uuid
price
name
description
quantity
slug
category {
name
}
images {
edges {
node {
uuid
image
}
}
}
category {
name
}
attributeGroups {
edges {
node {
name
uuid
attributes {
name
uuid
values {
value
uuid
}
}
}
}
}
}
}
}
}
...Order
}
}
}
${ORDER_FRAGMENT}
`
export const REMOVE_KIND_FROM_CART = gql`
@ -144,61 +45,11 @@ export const REMOVE_KIND_FROM_CART = gql`
productUuid: $productUuid
) {
order {
status
uuid
totalPrice
orderProducts {
edges {
node {
uuid
notifications
attributes
quantity
status
product {
uuid
price
name
description
quantity
slug
category {
name
}
images {
edges {
node {
uuid
image
}
}
}
category {
name
}
attributeGroups {
edges {
node {
name
uuid
attributes {
name
uuid
values {
value
uuid
}
}
}
}
}
}
}
}
}
...Order
}
}
}
${ORDER_FRAGMENT}
`
export const REMOVE_ALL_FROM_CART = gql`
@ -209,59 +60,9 @@ export const REMOVE_ALL_FROM_CART = gql`
orderUuid: $orderUuid
) {
order {
status
uuid
totalPrice
orderProducts {
edges {
node {
uuid
notifications
attributes
quantity
status
product {
uuid
price
name
description
quantity
slug
category {
name
}
images {
edges {
node {
uuid
image
}
}
}
category {
name
}
attributeGroups {
edges {
node {
name
uuid
attributes {
name
uuid
values {
value
uuid
}
}
}
}
}
}
}
}
}
...Order
}
}
}
${ORDER_FRAGMENT}
`

View file

@ -2,14 +2,16 @@ import gql from 'graphql-tag'
export const CONTACT_US = gql`
mutation contactUs(
$email: String!,
$name: String!,
$email: String!,
$phoneNumber: String,
$subject: String!,
$message: String!,
) {
contactUs(
email: $email,
name: $name,
email: $email,
phoneNumber: $phoneNumber,
subject: $subject,
message: $message
) {

View file

@ -0,0 +1,17 @@
import gql from "graphql-tag";
export const SWITCH_LANGUAGE = gql`
mutation setlanguage(
$uuid: UUID!,
$language: String,
) {
updateUser(
uuid: $uuid,
language: $language
) {
user {
...User
}
}
}
`

View file

@ -0,0 +1,41 @@
import gql from 'graphql-tag'
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
}
}
}
`

View file

@ -1,4 +1,5 @@
import gql from 'graphql-tag'
import {WISHLIST_FRAGMENT} from "@/graphql/fragments/wishlist.fragment.js";
export const ADD_TO_WISHLIST = gql`
mutation addToWishlist(
@ -10,30 +11,11 @@ export const ADD_TO_WISHLIST = gql`
productUuid: $productUuid
) {
wishlist {
uuid
products {
edges {
node {
uuid
price
name
description
quantity
slug
images {
edges {
node {
uuid
image
}
}
}
}
}
}
...Wishlist
}
}
}
${WISHLIST_FRAGMENT}
`
export const REMOVE_FROM_WISHLIST = gql`
@ -46,93 +28,24 @@ export const REMOVE_FROM_WISHLIST = gql`
productUuid: $productUuid
) {
wishlist {
uuid
products {
edges {
node {
uuid
price
name
description
quantity
slug
images {
edges {
node {
uuid
image
}
}
}
}
}
}
...Wishlist
}
}
}
${WISHLIST_FRAGMENT}
`
export const REMOVE_ALL_FROM_WISHLIST = gql`
mutation removeAllFromCart(
mutation removeAllFromWishlist(
$wishlistUuid: String!
) {
removeAllWishlistProducts(
wishlistUuid: $wishlistUuid
) {
order {
status
uuid
totalPrice
orderProducts {
edges {
node {
uuid
notifications
attributes
quantity
status
product {
uuid
price
name
description
quantity
slug
category {
name
}
images {
edges {
node {
uuid
image
}
}
}
category {
name
}
attributeGroups {
edges {
node {
name
uuid
attributes {
name
uuid
values {
value
uuid
}
}
}
}
}
}
}
}
}
wishlist {
...Wishlist
}
}
}
${WISHLIST_FRAGMENT}
`

View file

@ -0,0 +1,29 @@
import gql from 'graphql-tag'
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

@ -1,19 +1,17 @@
import gql from 'graphql-tag'
import {CATEGORY_FRAGMENT} from "@/graphql/fragments/categories.fragment.js";
export const GET_CATEGORIES = gql`
query getCategories {
categories {
edges {
node {
name
uuid
image
description
slug
...Category
}
}
}
}
${CATEGORY_FRAGMENT}
`
export const GET_CATEGORY_BY_SLUG = gql`
@ -25,11 +23,7 @@ export const GET_CATEGORY_BY_SLUG = gql`
) {
edges {
node {
name
uuid
image
description
slug
...Category
filterableAttributes {
possibleValues
attributeName
@ -42,4 +36,5 @@ export const GET_CATEGORY_BY_SLUG = gql`
}
}
}
${CATEGORY_FRAGMENT}
`

View file

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

View file

@ -1,17 +0,0 @@
import gql from 'graphql-tag'
export const GET_DOCS = gql`
query getDocs(
$slug: String!
) {
posts(
slug: $slug
) {
edges {
node {
content
}
}
}
}
`

View file

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

View file

@ -1,4 +1,5 @@
import gql from 'graphql-tag'
import {ORDER_FRAGMENT} from "@/graphql/fragments/orders.fragment.js";
export const GET_ORDERS = gql`
query getOrders(
@ -12,63 +13,10 @@ export const GET_ORDERS = gql`
) {
edges {
node {
totalPrice
uuid
status
buyTime
totalPrice
humanReadableId
orderProducts {
edges {
node {
uuid
notifications
attributes
quantity
status
product {
uuid
price
name
description
quantity
slug
category {
name
}
images {
edges {
node {
uuid
image
}
}
}
category {
name
}
attributeGroups {
edges {
node {
name
uuid
attributes {
name
uuid
values {
value
uuid
}
}
}
}
}
}
}
}
}
...Order
}
}
}
}
${ORDER_FRAGMENT}
`

View file

@ -1,13 +1,14 @@
import gql from 'graphql-tag'
import {PRODUCT_FRAGMENT} from "@/graphql/fragments/products.fragment.js";
export const GET_PRODUCTS = gql`
query getProducts(
$after: String,
$first: Number,
$first: Int,
$categorySlugs: String,
$orderBy: String,
$minPrice: String,
$maxPrice: String,
$minPrice: Decimal,
$maxPrice: Decimal,
$name: String
) {
products(
@ -22,34 +23,7 @@ export const GET_PRODUCTS = gql`
edges {
cursor
node {
uuid
name
price
quantity
slug
images {
edges {
node {
image
}
}
}
attributeGroups {
edges {
node {
name
uuid
attributes {
name
uuid
values {
value
uuid
}
}
}
}
}
...Product
}
}
pageInfo {
@ -58,6 +32,7 @@ export const GET_PRODUCTS = gql`
}
}
}
${PRODUCT_FRAGMENT}
`
export const GET_PRODUCT_BY_SLUG = gql`
@ -69,48 +44,10 @@ export const GET_PRODUCT_BY_SLUG = gql`
) {
edges {
node {
uuid
name
price
quantity
description
slug
category {
name
slug
}
images {
edges {
node {
image
}
}
}
attributeGroups {
edges {
node {
name
uuid
attributes {
name
uuid
values {
value
uuid
}
}
}
}
}
feedbacks {
edges {
node {
rating
}
}
}
...Product
}
}
}
}
${PRODUCT_FRAGMENT}
`

View file

@ -1,48 +1,15 @@
import gql from 'graphql-tag'
import {WISHLIST_FRAGMENT} from "@/graphql/fragments/wishlist.fragment.js";
export const GET_WISHLIST = gql`
query getWishlist {
wishlists {
edges {
node {
uuid
products {
edges {
node {
uuid
price
name
description
slug
images {
edges {
node {
uuid
image
}
}
}
attributeGroups {
edges {
node {
name
uuid
attributes {
name
uuid
values {
value
uuid
}
}
}
}
}
}
}
}
...Wishlist
}
}
}
}
${WISHLIST_FRAGMENT}
`

View file

@ -7,7 +7,9 @@
"goEmail": "Take me to my inbox",
"logout": "Log Out",
"buy": "Buy Now",
"save": "Save"
"save": "Save",
"sendLink": "Send link",
"topUp": "Top up"
},
"errors": {
"required": "This field is required!",
@ -22,36 +24,42 @@
},
"fields": {
"search": "Search Cards",
"name": "Name",
"firstName": "First name",
"lastName": "Last name",
"phone": "Phone number",
"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": {
"error": "Error!",
"login": {
"title": "Wellcome back!",
"text": "Sign in successes"
"errors": {
"main": "Error!",
"defaultError": "Something went wrong..",
"noDataToUpdate": "There is no data to update."
},
"register": {
"title": "Welcome!",
"text": "Account successfully created. Please confirm your Email before Sign In!"
"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",
"contactSuccess": "Your message was sent successfully!",
"activationSuccess": "E-mail successfully verified. Please Sign In!",
"successUpdate": "Profile successfully updated!",
"confirmEmail": "Verification E-mail link successfully sent!",
"payment": "Your purchase is being processed! Please stand by",
"reset": "If specified email exists in our system, we will send a password recovery email!",
"successNewPassword": "You have successfully changed your password!",
"successCheckout": "Order purchase successful!",
"addToWishlist": "{product} has been added to the wishlist!"
}

View file

@ -0,0 +1,24 @@
<template>
<div class="blog">
<div class="container">
<div class="blog__wrapper">
</div>
</div>
</div>
</template>
<script setup>
import {usePosts} from "@/composables/blog";
import {onMounted} from "vue";
const { posts, loading, getPosts } = usePosts();
onMounted(async () => {
await getPosts()
})
</script>
<style lang="scss" scoped>
</style>

View file

@ -5,7 +5,21 @@
</template>
<script setup>
import {onMounted} from "vue";
import {useRoute} from "vue-router";
import {useUserActivation} from "@/composables/user";
import DepositForm from "@/components/forms/deposit-form.vue";
import LoginForm from "@/components/forms/login-form.vue";
const route = useRoute()
const { activateUser } = useUserActivation();
onMounted( async () => {
if (route.name === "activate-user") {
await activateUser()
}
})
</script>
<style lang="scss" scoped>

View file

@ -0,0 +1,29 @@
<template>
<div class="post">
<div class="container">
<div class="post__wrapper">
</div>
</div>
</div>
</template>
<script setup>
import {computed, onMounted} from "vue";
import {usePostbySlug} from "@/composables/blog/usePostBySlug.js";
import {useRoute} from "vue-router";
const route = useRoute()
const slug = computed(() => route.params.postSlug)
const { post, loading, getPost } = usePostbySlug();
onMounted(async () => {
await getPost(slug.value)
})
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,29 @@
<template>
<div class="product">
<div class="container">
<div class="product__wrapper">
</div>
</div>
</div>
</template>
<script setup>
import {computed, onMounted} from "vue";
import {useRoute} from "vue-router";
import {useProductbySlug} from "@/composables/products/useProductBySlug.js";
const route = useRoute()
const slug = computed(() => route.params.productSlug)
const { product, loading, getProduct } = useProductbySlug();
onMounted(async () => {
await getProduct(slug.value)
})
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,19 @@
<template>
<div class="profile">
<div class="container">
<div class="profile__wrapper">
</div>
</div>
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.profile {
}
</style>

View file

@ -0,0 +1,28 @@
<template>
<div class="store">
<div class="container">
<div class="store__wrapper">
</div>
</div>
</div>
</template>
<script setup>
import {onMounted} from "vue";
import {useProducts} from "@/composables/products/index.js";
const { products, pageInfo, loading, getProducts } = useProducts();
onMounted(async () => {
await getProducts({})
console.log('products:', products)
console.log('pageInfo:', pageInfo)
})
</script>
<style lang="scss" scoped>
.store {
}
</style>

View file

@ -2,6 +2,16 @@ import {createRouter, createWebHistory, RouterView} from 'vue-router'
import HomePage from "@/pages/home-page.vue";
import translation from "@/core/helpers/translations.js";
import {APP_NAME} from "@/config/index.js";
import NewPasswordForm from "@/components/forms/new-password-form.vue";
import BlogPage from "@/pages/blog-page.vue";
import PostPage from "@/pages/post-page.vue";
import ProfilePage from "@/pages/profile-page.vue";
import {useAuthStore} from "@/stores/auth.js";
import RegisterForm from "@/components/forms/register-form.vue";
import LoginForm from "@/components/forms/login-form.vue";
import ResetPasswordForm from "@/components/forms/reset-password-form.vue";
import StorePage from "@/pages/store-page.vue";
import ProductPage from "@/pages/product-page.vue";
const routes = [
{
@ -16,6 +26,90 @@ const routes = [
meta: {
title: "Home"
}
},
{
path: 'activate-user',
name: 'activate-user',
component: HomePage,
meta: {
title: 'Home'
}
},
{
path: 'reset-password',
name: 'reset-password',
component: NewPasswordForm,
meta: {
title: 'New Password'
}
},
{
path: 'register',
name: 'register',
component: RegisterForm,
meta: {
title: 'Register',
requiresGuest: true
}
},
{
path: 'login',
name: 'login',
component: LoginForm,
meta: {
title: 'Login',
requiresGuest: true
}
},
{
path: 'forgot-password',
name: 'forgot-password',
component: ResetPasswordForm,
meta: {
title: 'Forgot Password',
requiresGuest: true
}
},
{
path: 'blog',
name: 'blog',
component: BlogPage,
meta: {
title: 'Blog'
}
},
{
path: 'blog/post/:postSlug',
name: 'blog-post',
component: PostPage,
meta: {
title: 'Post'
}
},
{
path: 'store',
name: 'store',
component: StorePage,
meta: {
title: 'Store'
}
},
{
path: 'product/:productSlug',
name: 'product',
component: ProductPage,
meta: {
title: 'Product'
}
},
{
path: 'profile',
name: 'profile',
component: ProfilePage,
meta: {
title: 'Profile',
requiresAuth: true
}
}
]
}
@ -34,8 +128,24 @@ const router = createRouter({
}
})
router.beforeEach((to, from) => {
router.beforeEach((to, from, next) => {
const authStore = useAuthStore();
const isAuthenticated = authStore.accessToken
document.title = to.meta.title ? `${APP_NAME} | ` + to.meta?.title : APP_NAME
if (to.meta.requiresAuth && !isAuthenticated) {
return next({
name: 'home',
query: { redirect: to.fullPath }
});
}
if (to.meta.requiresGuest && isAuthenticated) {
return next({ name: 'home' });
}
next();
})
export default router

View file

@ -2,13 +2,13 @@ import {defineStore} from "pinia";
import {ref} from "vue";
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const accessToken = ref(null)
const user = ref(null);
const accessToken = ref(null);
const setUser = (payload) => {
user.value = payload.user
accessToken.value = payload.accessToken
}
const setUser = (payload) => {
user.value = payload.user
accessToken.value = payload.accessToken
}
return { user, accessToken, setUser }
return { user, accessToken, setUser }
})

View file

@ -2,10 +2,10 @@ import {defineStore} from "pinia";
import {ref} from "vue";
export const useCartStore = defineStore('cart', () => {
const currentOrder = ref({})
const currentOrder = ref({});
const setCurrentOrders = (order) => {
currentOrder.value = order
}
};
return {
currentOrder,

View file

@ -2,10 +2,10 @@ import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCompanyStore = defineStore('company', () => {
const companyInfo = ref([])
const companyInfo = ref([]);
const setCompanyInfo = (payload) => {
companyInfo.value = payload
}
};
return {
companyInfo,

View file

@ -0,0 +1,14 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useLanguageStore = defineStore('language', () => {
const languages = ref([]);
const setLanguages = (payload) => {
languages.value = payload
};
return {
languages,
setLanguages
}
})

View file

@ -2,10 +2,10 @@ import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useWishlistStore = defineStore('wishlist', () => {
const wishlist = ref({})
const wishlist = ref({});
const setWishlist = (order) => {
wishlist.value = order
}
};
return {
wishlist,