Features: 1) Add product rating support in types, GraphQL fragments, and UI components; 2) Implement feedback management including GraphQL mutations, composables, and notification handling; 3) Enhance locale switching with improved reactivity, Apollo query clearing, and supported locale validation; 4) Introduce useOrderBuy composable for order purchasing workflow.
Fixes: 1) Correct mutation name from `setlanguage` to `setLanguage` for consistency; 2) Improve product listing reactivity by addressing missing initialization in `useStore`; 3) Replace generic product queries with parametrized `useProducts` for modularity; 4) Resolve minor typos, missing semicolons, and code formatting inconsistencies. Extra: 1) Refactor feedback-related types, composables, and GraphQL utilities for modularity; 2) Update styles, Vue templates, and related scripts with enhanced formatting; 3) Remove unused methods like `getProducts`, standardizing query reactivity; 4) Cleanup and organize imports across multiple files.
This commit is contained in:
parent
949e077942
commit
c9807bd6d4
34 changed files with 418 additions and 154 deletions
|
|
@ -20,7 +20,7 @@
|
||||||
import {useAppConfig} from "~/composables/config";
|
import {useAppConfig} from "~/composables/config";
|
||||||
import { DEFAULT_LOCALE } from '~/config/constants';
|
import { DEFAULT_LOCALE } from '~/config/constants';
|
||||||
import {useRefresh} from "~/composables/auth";
|
import {useRefresh} from "~/composables/auth";
|
||||||
import {useLanguages} from "~/composables/languages";
|
import {useLanguages, useLocaleRedirect} from "~/composables/languages";
|
||||||
import {useCompanyInfo} from "~/composables/company";
|
import {useCompanyInfo} from "~/composables/company";
|
||||||
import {useCategories} from "~/composables/categories";
|
import {useCategories} from "~/composables/categories";
|
||||||
|
|
||||||
|
|
@ -55,6 +55,7 @@ const cookieLocale = useCookie(
|
||||||
|
|
||||||
const { refresh } = useRefresh();
|
const { refresh } = useRefresh();
|
||||||
const { getCategories } = await useCategories();
|
const { getCategories } = await useCategories();
|
||||||
|
const { isSupportedLocale } = useLocaleRedirect();
|
||||||
|
|
||||||
let refreshInterval: NodeJS.Timeout;
|
let refreshInterval: NodeJS.Timeout;
|
||||||
|
|
||||||
|
|
@ -83,20 +84,32 @@ watch(locale, () => {
|
||||||
|
|
||||||
let stopWatcher: VoidFunction = () => {};
|
let stopWatcher: VoidFunction = () => {};
|
||||||
|
|
||||||
|
if (!cookieLocale.value) {
|
||||||
|
cookieLocale.value = DEFAULT_LOCALE;
|
||||||
|
await router.push({path: switchLocalePath(cookieLocale.value)});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locale.value !== cookieLocale.value) {
|
||||||
|
if (isSupportedLocale(cookieLocale.value)) {
|
||||||
|
await router.push({
|
||||||
|
path: switchLocalePath(cookieLocale.value),
|
||||||
|
query: route.query
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cookieLocale.value = DEFAULT_LOCALE
|
||||||
|
|
||||||
|
await router.push({
|
||||||
|
path: switchLocalePath(DEFAULT_LOCALE),
|
||||||
|
query: route.query
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted( async () => {
|
onMounted( async () => {
|
||||||
refreshInterval = setInterval(async () => {
|
refreshInterval = setInterval(async () => {
|
||||||
await refresh();
|
await refresh();
|
||||||
}, 600000);
|
}, 600000);
|
||||||
|
|
||||||
if (!cookieLocale.value) {
|
|
||||||
cookieLocale.value = DEFAULT_LOCALE;
|
|
||||||
await router.push({path: switchLocalePath(cookieLocale.value)});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locale.value !== cookieLocale.value) {
|
|
||||||
await router.push({path: switchLocalePath(cookieLocale.value)});
|
|
||||||
}
|
|
||||||
|
|
||||||
stopWatcher = watch(
|
stopWatcher = watch(
|
||||||
() => appStore.isOverflowHidden,
|
() => appStore.isOverflowHidden,
|
||||||
(hidden) => {
|
(hidden) => {
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const companyStore = useCompanyStore()
|
const companyStore = useCompanyStore();
|
||||||
const { t } = useI18n()
|
const { t } = useI18n();
|
||||||
|
|
||||||
const companyInfo = computed(() => companyStore.companyInfo)
|
const companyInfo = computed(() => companyStore.companyInfo);
|
||||||
|
|
||||||
const encodedCompanyAddress = computed(() => {
|
const encodedCompanyAddress = computed(() => {
|
||||||
return companyInfo.value?.companyAddress ? encodeURIComponent(companyInfo.value?.companyAddress) : ''
|
return companyInfo.value?.companyAddress ? encodeURIComponent(companyInfo.value?.companyAddress) : '';
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -59,25 +59,25 @@ import {onClickOutside} from "@vueuse/core";
|
||||||
import type {ICategory} from "~/types";
|
import type {ICategory} from "~/types";
|
||||||
import {useCategoryStore} from "~/stores/category";
|
import {useCategoryStore} from "~/stores/category";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n();
|
||||||
const categoryStore = useCategoryStore();
|
const categoryStore = useCategoryStore();
|
||||||
|
|
||||||
const categories = computed(() => categoryStore.categories)
|
const categories = computed(() => categoryStore.categories);
|
||||||
|
|
||||||
const isBlockOpen = ref<boolean>(false)
|
const isBlockOpen = ref<boolean>(false);
|
||||||
const setBlock = (state: boolean) => {
|
const setBlock = (state: boolean) => {
|
||||||
isBlockOpen.value = state
|
isBlockOpen.value = state;
|
||||||
}
|
};
|
||||||
|
|
||||||
// TODO: add loading state
|
// TODO: add loading state
|
||||||
|
|
||||||
const blockRef = ref(null)
|
const blockRef = ref(null);
|
||||||
onClickOutside(blockRef, () => setBlock(false))
|
onClickOutside(blockRef, () => setBlock(false));
|
||||||
|
|
||||||
const activeCategory = ref<ICategory>(categories.value[0]?.node)
|
const activeCategory = ref<ICategory>(categories.value[0]?.node);
|
||||||
const setActiveCategory = (category: ICategory) => {
|
const setActiveCategory = (category: ICategory) => {
|
||||||
activeCategory.value = category
|
activeCategory.value = category;
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -42,19 +42,19 @@
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import {useValidators} from "~/composables/rules";
|
import {useValidators} from "~/composables/rules";
|
||||||
import {useContactUs} from "~/composables/contact/index.js";
|
import {useContactUs} from "~/composables/contact/index.js";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n();
|
||||||
|
|
||||||
const { required } = useValidators()
|
const { required } = useValidators();
|
||||||
|
|
||||||
const name = ref('')
|
const name = ref<string>('');
|
||||||
const email = ref('')
|
const email = ref<string>('');
|
||||||
const phoneNumber = ref('')
|
const phoneNumber = ref<string>('');
|
||||||
const subject = ref('')
|
const subject = ref<string>('');
|
||||||
const message = ref('')
|
const message = ref<string>('');
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -63,8 +63,8 @@ const isFormValid = computed(() => {
|
||||||
required(phoneNumber.value) === true &&
|
required(phoneNumber.value) === true &&
|
||||||
required(subject.value) === true &&
|
required(subject.value) === true &&
|
||||||
required(message.value) === true
|
required(message.value) === true
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
const { contactUs, loading } = useContactUs();
|
const { contactUs, loading } = useContactUs();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,28 +24,28 @@
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import {useValidators} from "~/composables/rules/index.js";
|
import {useValidators} from "~/composables/rules/index.js";
|
||||||
import {useNewPassword} from "@/composables/auth";
|
import {useNewPassword} from "@/composables/auth";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n();
|
||||||
|
|
||||||
const { isPasswordValid } = useValidators()
|
const { isPasswordValid } = useValidators();
|
||||||
|
|
||||||
const password = ref('')
|
const password = ref<string>('');
|
||||||
const confirmPassword = ref('')
|
const confirmPassword = ref<string>('');
|
||||||
|
|
||||||
const compareStrings = (v) => {
|
const compareStrings = (v: string) => {
|
||||||
if (v === password.value) return true
|
if (v === password.value) return true;
|
||||||
return t('errors.compare')
|
return t('errors.compare');
|
||||||
}
|
};
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return (
|
return (
|
||||||
isPasswordValid(password.value) === true &&
|
isPasswordValid(password.value) === true &&
|
||||||
compareStrings(confirmPassword.value) === true
|
compareStrings(confirmPassword.value) === true
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
const { newPassword, loading } = useNewPassword();
|
const { newPassword, loading } = useNewPassword();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,21 +19,21 @@
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import {useValidators} from "~/composables/rules";
|
import {useValidators} from "~/composables/rules";
|
||||||
import {usePasswordReset} from "@/composables/auth";
|
import {usePasswordReset} from "@/composables/auth";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n();
|
||||||
|
|
||||||
const { isEmail } = useValidators()
|
const { isEmail } = useValidators();
|
||||||
|
|
||||||
const email = ref('')
|
const email = ref<string>('');
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return (
|
return (
|
||||||
isEmail(email.value) === true
|
isEmail(email.value) === true
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
const { resetPassword, loading } = usePasswordReset();
|
const { resetPassword, loading } = usePasswordReset();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const img = useImage()
|
const img = useImage();
|
||||||
|
|
||||||
const backgroundStyles = computed(() => {
|
const backgroundStyles = computed(() => {
|
||||||
const imgUrl = img('/images/homeBg.png', { format: 'webp', densities: 'x1 x2' })
|
const imgUrl = img('/images/homeBg.png', { format: 'webp', densities: 'x1 x2' });
|
||||||
|
|
||||||
return { backgroundImage: `url('${imgUrl}')` }
|
return { backgroundImage: `url('${imgUrl}')` };
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<client-only>
|
<client-only>
|
||||||
<el-breadcrumb separator="/" class="breadcrumbs">
|
<el-breadcrumb separator="/" class="breadcrumbs">
|
||||||
<el-breadcrumb-item
|
<el-breadcrumb-item
|
||||||
v-for="(crumb, idx) in breadcrumbs"
|
v-for="(crumb, idx) in breadcrumbs"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
>
|
|
||||||
<nuxt-link-locale
|
|
||||||
v-if="idx !== breadcrumbs.length - 1"
|
|
||||||
:to="crumb.link"
|
|
||||||
class="breadcrumbs__link"
|
|
||||||
>
|
>
|
||||||
{{ crumb.text }}
|
<nuxt-link-locale
|
||||||
</nuxt-link-locale>
|
v-if="idx !== breadcrumbs.length - 1"
|
||||||
<span v-else class="breadcrumbs__current">
|
:to="crumb.link"
|
||||||
{{ crumb.text }}
|
class="breadcrumbs__link"
|
||||||
</span>
|
>
|
||||||
</el-breadcrumb-item>
|
{{ crumb.text }}
|
||||||
</el-breadcrumb>
|
</nuxt-link-locale>
|
||||||
|
<span v-else class="breadcrumbs__current">
|
||||||
|
{{ crumb.text }}
|
||||||
|
</span>
|
||||||
|
</el-breadcrumb-item>
|
||||||
|
</el-breadcrumb>
|
||||||
</client-only>
|
</client-only>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {GET_CATEGORIES} from "~/graphql/queries/standalone/categories";
|
||||||
|
|
||||||
export async function useCategories() {
|
export async function useCategories() {
|
||||||
const categoryStore = useCategoryStore();
|
const categoryStore = useCategoryStore();
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
const getCategories = async (cursor?: string): Promise<void> => {
|
const getCategories = async (cursor?: string): Promise<void> => {
|
||||||
const {data, error} = await useAsyncQuery<ICategoriesResponse>(
|
const {data, error} = await useAsyncQuery<ICategoriesResponse>(
|
||||||
|
|
@ -31,6 +32,11 @@ export async function useCategories() {
|
||||||
if (error.value) console.error('useCategories error:', error.value);
|
if (error.value) console.error('useCategories error:', error.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(locale, async () => {
|
||||||
|
categoryStore.setCategories([]);
|
||||||
|
await getCategories();
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getCategories
|
getCategories
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,37 @@ import type {IContactUsResponse} from "~/types";
|
||||||
import {CONTACT_US} from "~/graphql/mutations/contact";
|
import {CONTACT_US} from "~/graphql/mutations/contact";
|
||||||
import {useNotification} from "~/composables/notification";
|
import {useNotification} from "~/composables/notification";
|
||||||
|
|
||||||
|
interface IContactUsArguments {
|
||||||
|
name: string,
|
||||||
|
email: string,
|
||||||
|
phoneNumber?: string,
|
||||||
|
subject?: string,
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
export function useContactUs() {
|
export function useContactUs() {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
const { mutate, loading, error } = useMutation<IContactUsResponse>(CONTACT_US);
|
const { mutate, loading, error } = useMutation<IContactUsResponse>(CONTACT_US);
|
||||||
|
|
||||||
async function contactUs(
|
async function contactUs(
|
||||||
name: string,
|
args: IContactUsArguments
|
||||||
email: string,
|
|
||||||
phoneNumber: string,
|
|
||||||
subject: string,
|
|
||||||
message: string
|
|
||||||
) {
|
) {
|
||||||
const result = await mutate({
|
const variables: Record<string, any> = {
|
||||||
name,
|
name: args.name,
|
||||||
email,
|
email: args.email,
|
||||||
phoneNumber,
|
message: args.message
|
||||||
subject,
|
};
|
||||||
message
|
|
||||||
});
|
if (args.phoneNumber) {
|
||||||
|
variables.phoneNumber = args.phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.subject) {
|
||||||
|
variables.subject = args.subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await mutate(variables);
|
||||||
|
|
||||||
if (result?.data?.contactUs.received) {
|
if (result?.data?.contactUs.received) {
|
||||||
useNotification({
|
useNotification({
|
||||||
|
|
|
||||||
1
storefront/composables/feedbacks/index.ts
Normal file
1
storefront/composables/feedbacks/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './useFeedbackAction'
|
||||||
74
storefront/composables/feedbacks/useFeedbackAction.ts
Normal file
74
storefront/composables/feedbacks/useFeedbackAction.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
import type {IFeedbackActionResponse} from "~/types";
|
||||||
|
import {MANAGE_FEEDBACK} from "~/graphql/mutations/feedbacks";
|
||||||
|
import {isGraphQLError} from "~/utils/error";
|
||||||
|
|
||||||
|
interface IFeedbackActionArguments {
|
||||||
|
action: string,
|
||||||
|
orderProductUuid: string,
|
||||||
|
comment?: string,
|
||||||
|
rating?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFeedbackAction() {
|
||||||
|
const {t} = useI18n();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const isAuthenticated = computed(() => userStore.isAuthenticated);
|
||||||
|
|
||||||
|
const { mutate, loading, error } = useMutation<IFeedbackActionResponse>(MANAGE_FEEDBACK);
|
||||||
|
|
||||||
|
async function manageFeedback(
|
||||||
|
args: IFeedbackActionArguments
|
||||||
|
) {
|
||||||
|
if (isAuthenticated.value) {
|
||||||
|
const variables: Record<string, any> = {
|
||||||
|
action: args.action,
|
||||||
|
orderProductUuid: args.orderProductUuid,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args.comment) {
|
||||||
|
variables.comment = args.comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.rating) {
|
||||||
|
variables.rating = args.rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await mutate(variables);
|
||||||
|
|
||||||
|
if (result?.data?.feedback) {
|
||||||
|
useNotification({
|
||||||
|
message: t('popup.success.addFeedback'),
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
useNotification({
|
||||||
|
message: t('popup.errors.loginFirst'),
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(error, (err) => {
|
||||||
|
if (!err) return;
|
||||||
|
console.error('useFeedbackAction error:', err);
|
||||||
|
let message = t('popup.errors.defaultError');
|
||||||
|
if (isGraphQLError(err)) {
|
||||||
|
message = err.graphQLErrors?.[0]?.message || message;
|
||||||
|
} else {
|
||||||
|
message = err.message;
|
||||||
|
}
|
||||||
|
useNotification({
|
||||||
|
message,
|
||||||
|
type: 'error',
|
||||||
|
title: t('popup.errors.main')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return{
|
||||||
|
loading,
|
||||||
|
manageFeedback
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import {DEFAULT_LOCALE} from "@intlify/core-base";
|
||||||
export function useLanguageSwitch() {
|
export function useLanguageSwitch() {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { $apollo } = useNuxtApp() as any;
|
||||||
|
|
||||||
const { COOKIES_LOCALE_KEY } = useAppConfig();
|
const { COOKIES_LOCALE_KEY } = useAppConfig();
|
||||||
const switchLocalePath = useSwitchLocalePath();
|
const switchLocalePath = useSwitchLocalePath();
|
||||||
|
|
@ -23,21 +24,42 @@ export function useLanguageSwitch() {
|
||||||
|
|
||||||
const { mutate, loading, error } = useMutation<IUserResponse>(SWITCH_LANGUAGE);
|
const { mutate, loading, error } = useMutation<IUserResponse>(SWITCH_LANGUAGE);
|
||||||
|
|
||||||
|
let isSwitching = false;
|
||||||
|
|
||||||
async function switchLanguage(
|
async function switchLanguage(
|
||||||
locale: string
|
locale: string
|
||||||
) {
|
) {
|
||||||
cookieLocale.value = locale;
|
if (isSwitching || cookieLocale.value === locale) return;
|
||||||
await router.push({path: switchLocalePath(cookieLocale.value as LocaleDefinition['code'])})
|
|
||||||
|
|
||||||
if (isAuthenticated.value) {
|
try {
|
||||||
const result = await mutate({
|
isSwitching = true;
|
||||||
uuid: userUuid.value,
|
|
||||||
locale
|
cookieLocale.value = locale;
|
||||||
|
|
||||||
|
await $apollo.defaultClient.clearStore();
|
||||||
|
|
||||||
|
await router.push({path: switchLocalePath(cookieLocale.value as LocaleDefinition['code'])});
|
||||||
|
|
||||||
|
if (isAuthenticated.value) {
|
||||||
|
const result = await mutate({
|
||||||
|
uuid: userUuid.value,
|
||||||
|
locale
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.data?.updateUser) {
|
||||||
|
userStore.setUser(result.data.updateUser.user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
await $apollo.defaultClient.refetchQueries({
|
||||||
|
include: 'active'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.data?.updateUser) {
|
} catch (error) {
|
||||||
userStore.setUser(result.data.updateUser.user)
|
console.error('Error switching language:', error);
|
||||||
}
|
} finally {
|
||||||
|
isSwitching = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export function useLocaleRedirect() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isSupportedLocale,
|
||||||
checkAndRedirect
|
checkAndRedirect
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -23,10 +23,14 @@ export function useNotification(
|
||||||
|
|
||||||
const messageVNode = h('div', [bodyContent, progressBar])
|
const messageVNode = h('div', [bodyContent, progressBar])
|
||||||
|
|
||||||
ElNotification({
|
const notification = ElNotification({
|
||||||
title: args.title,
|
title: args.title,
|
||||||
duration,
|
duration: 0,
|
||||||
message: messageVNode,
|
message: messageVNode,
|
||||||
type: args.type
|
type: args.type
|
||||||
} as NotificationOptions)
|
} as NotificationOptions)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.close()
|
||||||
|
}, duration)
|
||||||
}
|
}
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './useOrderOverwrite';
|
export * from './useOrderOverwrite';
|
||||||
export * from './useOrders';
|
export * from './useOrders';
|
||||||
|
export * from './useOrderBuy';
|
||||||
46
storefront/composables/orders/useOrderBuy.ts
Normal file
46
storefront/composables/orders/useOrderBuy.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import {isGraphQLError} from "~/utils/error";
|
||||||
|
import type {IBuyOrderResponse} from "~/types";
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
import {BUY_CART} from "~/graphql/mutations/cart";
|
||||||
|
|
||||||
|
export function useOrderBuy() {
|
||||||
|
const {t} = useI18n();
|
||||||
|
|
||||||
|
const { mutate, loading, error } = useMutation<IBuyOrderResponse>(BUY_CART);
|
||||||
|
|
||||||
|
async function buyOrder(
|
||||||
|
orderUuid: string,
|
||||||
|
) {
|
||||||
|
const result = await mutate({
|
||||||
|
orderUuid,
|
||||||
|
forcePayment: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.data?.buyOrder?.transaction?.process?.redirect_url) {
|
||||||
|
window.location.href = result.data.buyOrder.transaction.process.redirect_url
|
||||||
|
} else {
|
||||||
|
console.log(result?.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(error, (err) => {
|
||||||
|
if (!err) return;
|
||||||
|
console.error('useOrderBuy error:', err);
|
||||||
|
let message = t('popup.errors.defaultError');
|
||||||
|
if (isGraphQLError(err)) {
|
||||||
|
message = err.graphQLErrors?.[0]?.message || message;
|
||||||
|
} else {
|
||||||
|
message = err.message;
|
||||||
|
}
|
||||||
|
useNotification({
|
||||||
|
message,
|
||||||
|
type: 'error',
|
||||||
|
title: t('popup.errors.main')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
buyOrder,
|
||||||
|
loading
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,29 +1,34 @@
|
||||||
import { GET_PRODUCTS } from '~/graphql/queries/standalone/products';
|
import { GET_PRODUCTS } from '~/graphql/queries/standalone/products';
|
||||||
import type { IProductResponse } from '~/types';
|
import type { IProductResponse } from '~/types';
|
||||||
|
|
||||||
export function useProducts() {
|
export function useProducts(params: Record<string, any> = {}) {
|
||||||
const variables = ref({ first: 12 });
|
const variables = ref({
|
||||||
|
first: 12,
|
||||||
|
...params
|
||||||
|
});
|
||||||
|
|
||||||
const { data, error, refresh } = useAsyncQuery<IProductResponse>(
|
const { data, error } = useAsyncQuery<IProductResponse>(
|
||||||
GET_PRODUCTS,
|
GET_PRODUCTS,
|
||||||
variables
|
variables
|
||||||
);
|
);
|
||||||
|
|
||||||
const products = computed(() => data.value?.products?.edges ?? []);
|
const products = computed(() => data.value?.products?.edges ?? []);
|
||||||
const pageInfo = computed(() => data.value?.products?.pageInfo ?? {});
|
const pageInfo = computed(() => data.value?.products?.pageInfo ?? {});
|
||||||
|
|
||||||
const getProducts = async (params: Record<string, any> = {}) => {
|
|
||||||
variables.value = { ...variables.value, ...params };
|
|
||||||
await refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(error, (e) => {
|
watch(error, (e) => {
|
||||||
if (e) console.error('useProducts error:', e);
|
if (e) console.error('useProducts error:', e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateVariables = (newParams: Record<string, any>) => {
|
||||||
|
variables.value = {
|
||||||
|
first: 12,
|
||||||
|
...newParams
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
products,
|
products,
|
||||||
pageInfo,
|
pageInfo,
|
||||||
getProducts
|
updateVariables
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -12,12 +12,12 @@ interface IProdVars {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useStore(
|
export function useStore(
|
||||||
slug: string,
|
slug: string,
|
||||||
attributes?: string,
|
attributes?: string,
|
||||||
orderBy?: string,
|
orderBy?: string,
|
||||||
minPrice?: number,
|
minPrice?: number,
|
||||||
maxPrice?: number,
|
maxPrice?: number,
|
||||||
productAfter?: string
|
productAfter?: string
|
||||||
) {
|
) {
|
||||||
const variables = reactive<IProdVars>({
|
const variables = reactive<IProdVars>({
|
||||||
first: 15,
|
first: 15,
|
||||||
|
|
@ -36,31 +36,47 @@ export function useStore(
|
||||||
|
|
||||||
const products = ref([...(data.value?.products.edges ?? [])]);
|
const products = ref([...(data.value?.products.edges ?? [])]);
|
||||||
const pageInfo = computed(() => data.value?.products.pageInfo ?? null);
|
const pageInfo = computed(() => data.value?.products.pageInfo ?? null);
|
||||||
|
const isInitialized = ref(false);
|
||||||
|
|
||||||
watch(error, e => e && console.error('useStore products error', e));
|
watch(error, e => e && console.error('useStore products error', e));
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => variables.productAfter,
|
data,
|
||||||
async (newCursor, oldCursor) => {
|
(newData) => {
|
||||||
if (!newCursor || newCursor === oldCursor) return;
|
if (!newData?.products?.edges) return;
|
||||||
await refresh();
|
|
||||||
const newEdges = data.value?.products.edges ?? [];
|
const newEdges = newData.products.edges;
|
||||||
products.value = [...products.value, ...newEdges];
|
|
||||||
}
|
if (!isInitialized.value || !variables.productAfter) {
|
||||||
|
products.value = [...newEdges];
|
||||||
|
isInitialized.value = true;
|
||||||
|
} else {
|
||||||
|
products.value = [...products.value, ...newEdges];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[
|
() => variables.productAfter,
|
||||||
() => variables.attributes,
|
async (newCursor, oldCursor) => {
|
||||||
() => variables.orderBy,
|
if (!newCursor || newCursor === oldCursor || !isInitialized.value) return;
|
||||||
() => variables.minPrice,
|
await refresh();
|
||||||
() => variables.maxPrice
|
}
|
||||||
],
|
);
|
||||||
async () => {
|
|
||||||
variables.productAfter = '';
|
watch(
|
||||||
await refresh();
|
[
|
||||||
products.value = [...(data.value?.products.edges ?? [])];
|
() => variables.categoriesSlugs,
|
||||||
}
|
() => variables.attributes,
|
||||||
|
() => variables.orderBy,
|
||||||
|
() => variables.minPrice,
|
||||||
|
() => variables.maxPrice
|
||||||
|
],
|
||||||
|
async () => {
|
||||||
|
variables.productAfter = '';
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
7
storefront/graphql/fragments/feedback.fragment.ts
Normal file
7
storefront/graphql/fragments/feedback.fragment.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const FEEDBACK_FRAGMENT = gql`
|
||||||
|
fragment Feedback on FeedbackType {
|
||||||
|
comment
|
||||||
|
rating
|
||||||
|
uuid
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
@ -4,6 +4,7 @@ export const PRODUCT_FRAGMENT = gql`
|
||||||
name
|
name
|
||||||
price
|
price
|
||||||
quantity
|
quantity
|
||||||
|
rating
|
||||||
slug
|
slug
|
||||||
description
|
description
|
||||||
brand {
|
brand {
|
||||||
|
|
|
||||||
|
|
@ -83,4 +83,22 @@ export const BULK_CART = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${ORDER_FRAGMENT}
|
${ORDER_FRAGMENT}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const BUY_CART = gql`
|
||||||
|
mutation buyOrder(
|
||||||
|
$orderUuid: String!,
|
||||||
|
$forcePayment: Boolean!
|
||||||
|
) {
|
||||||
|
buyOrder(
|
||||||
|
orderUuid: $orderUuid
|
||||||
|
forcePayment: $forcePayment
|
||||||
|
) {
|
||||||
|
transaction {
|
||||||
|
amount
|
||||||
|
process
|
||||||
|
paymentMethod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
22
storefront/graphql/mutations/feedbacks.ts
Normal file
22
storefront/graphql/mutations/feedbacks.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import {FEEDBACK_FRAGMENT} from "~/graphql/fragments/feedback.fragment";
|
||||||
|
|
||||||
|
export const MANAGE_FEEDBACK = gql`
|
||||||
|
mutation addFeedback(
|
||||||
|
$action: String!,
|
||||||
|
$comment: String,
|
||||||
|
$orderProductUuid: UUID!,
|
||||||
|
$rating: Int
|
||||||
|
) {
|
||||||
|
feedbackProductAction(
|
||||||
|
action: $action,
|
||||||
|
comment: $comment,
|
||||||
|
orderProductUuid: $orderProductUuid,
|
||||||
|
rating: $rating
|
||||||
|
) {
|
||||||
|
feedback {
|
||||||
|
...Feedback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${FEEDBACK_FRAGMENT}
|
||||||
|
`
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import {USER_FRAGMENT} from "~/graphql/fragments/user.fragment";
|
import {USER_FRAGMENT} from "~/graphql/fragments/user.fragment";
|
||||||
|
|
||||||
export const SWITCH_LANGUAGE = gql`
|
export const SWITCH_LANGUAGE = gql`
|
||||||
mutation setlanguage(
|
mutation setLanguage(
|
||||||
$uuid: UUID!,
|
$uuid: UUID!,
|
||||||
$language: String,
|
$language: String,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,8 @@
|
||||||
"avatarUpload": "You have successfully uploaded an avatar!",
|
"avatarUpload": "You have successfully uploaded an avatar!",
|
||||||
"userUpdate": "Profile successfully updated!",
|
"userUpdate": "Profile successfully updated!",
|
||||||
"referralCopy": "You copied your referal link!",
|
"referralCopy": "You copied your referal link!",
|
||||||
"promocodeCopy": "You copied your promocode!"
|
"promocodeCopy": "You copied your promocode!",
|
||||||
|
"addFeedback": "Your feedback has been saved!"
|
||||||
},
|
},
|
||||||
"addToCartLimit": "Total quantity limit is {quantity}!",
|
"addToCartLimit": "Total quantity limit is {quantity}!",
|
||||||
"failAdd": "Please log in to make a purchase",
|
"failAdd": "Please log in to make a purchase",
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export default defineNuxtConfig({
|
||||||
title: process.env.EVIBES_PROJECT_NAME,
|
title: process.env.EVIBES_PROJECT_NAME,
|
||||||
titleTemplate: `${process.env.EVIBES_PROJECT_NAME} | %s`,
|
titleTemplate: `${process.env.EVIBES_PROJECT_NAME} | %s`,
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
{ rel: 'icon', type: 'image/x-icon', href: `https://${process.env.EVIBES_BASE_DOMAIN}/favicon.ico` },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
pageTransition: {
|
pageTransition: {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import {useUserActivation} from "~/composables/user";
|
||||||
import { useRouteQuery } from '@vueuse/router';
|
import { useRouteQuery } from '@vueuse/router';
|
||||||
import {useBrands} from "~/composables/brands";
|
import {useBrands} from "~/composables/brands";
|
||||||
import {useProducts, useProductTags} from "~/composables/products";
|
import {useProducts, useProductTags} from "~/composables/products";
|
||||||
import type {IProduct} from "~/types";
|
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
|
@ -24,7 +23,7 @@ const route = useRoute();
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: t('breadcrumbs.home'),
|
title: t('breadcrumbs.home'),
|
||||||
})
|
});
|
||||||
|
|
||||||
const token = useRouteQuery('token', '');
|
const token = useRouteQuery('token', '');
|
||||||
const uid = useRouteQuery('uid', '');
|
const uid = useRouteQuery('uid', '');
|
||||||
|
|
@ -32,17 +31,10 @@ const referrer = useRouteQuery('referrer', '');
|
||||||
|
|
||||||
const { activateUser } = useUserActivation();
|
const { activateUser } = useUserActivation();
|
||||||
|
|
||||||
const newProducts = ref<{ cursor: string; node: IProduct }[]>([]);
|
|
||||||
const priceProducts = ref<{ cursor: string; node: IProduct }[]>([]);
|
|
||||||
const { brands } = useBrands();
|
const { brands } = useBrands();
|
||||||
const { tags } = useProductTags();
|
const { tags } = useProductTags();
|
||||||
const { products, getProducts } = useProducts();
|
const { products: newProducts } = useProducts({ orderBy: '-modified' });
|
||||||
|
const { products: priceProducts } = useProducts({ orderBy: '-price' });
|
||||||
await getProducts({ orderBy: '-modified' });
|
|
||||||
newProducts.value = products.value;
|
|
||||||
|
|
||||||
await getProducts({ orderBy: '-price' });
|
|
||||||
priceProducts.value = products.value;
|
|
||||||
|
|
||||||
onMounted( async () => {
|
onMounted( async () => {
|
||||||
if (route.path.includes('activate-user') && token.value && uid.value) {
|
if (route.path.includes('activate-user') && token.value && uid.value) {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
</div>
|
</div>
|
||||||
<el-rate
|
<el-rate
|
||||||
class="white"
|
class="white"
|
||||||
v-model="rating"
|
:model-value="product?.rating"
|
||||||
allow-half
|
allow-half
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
|
|
@ -224,10 +224,7 @@ if (meta) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { products, getProducts } = useProducts();
|
const { products } = useProducts({ categoriesSlugs: product.value?.category.slug });
|
||||||
await getProducts({
|
|
||||||
categoriesSlugs: product.value?.category.slug
|
|
||||||
});
|
|
||||||
|
|
||||||
const isProductInWishlist = computed(() => {
|
const isProductInWishlist = computed(() => {
|
||||||
const el = wishlistStore.wishlist?.products?.edges.find(
|
const el = wishlistStore.wishlist?.products?.edges.find(
|
||||||
|
|
@ -246,10 +243,6 @@ const images = computed<string[]>(() =>
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
|
|
||||||
const rating = computed(() => {
|
|
||||||
return product.value?.feedbacks.edges[0]?.node?.rating ?? 3;
|
|
||||||
});
|
|
||||||
|
|
||||||
const attributes = computed(() => {
|
const attributes = computed(() => {
|
||||||
const edges = product.value?.attributeGroups.edges ?? [];
|
const edges = product.value?.attributeGroups.edges ?? [];
|
||||||
|
|
||||||
|
|
|
||||||
5
storefront/types/api/feedbacks.ts
Normal file
5
storefront/types/api/feedbacks.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import type {IFeedback} from "~/types";
|
||||||
|
|
||||||
|
export interface IFeedbackActionResponse {
|
||||||
|
feedback: IFeedback
|
||||||
|
}
|
||||||
|
|
@ -40,4 +40,16 @@ export interface IBulkOrderResponse {
|
||||||
bulkOrderAction: {
|
bulkOrderAction: {
|
||||||
order: IOrder
|
order: IOrder
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBuyOrderResponse {
|
||||||
|
buyOrder: {
|
||||||
|
transaction: {
|
||||||
|
amount: number,
|
||||||
|
process: {
|
||||||
|
invoice_id: number,
|
||||||
|
redirect_url: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
5
storefront/types/app/feedbacks.ts
Normal file
5
storefront/types/app/feedbacks.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface IFeedback {
|
||||||
|
comment: string,
|
||||||
|
rating: number,
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,10 @@ export interface IOrder {
|
||||||
attributes: string,
|
attributes: string,
|
||||||
quantity: number,
|
quantity: number,
|
||||||
status: string,
|
status: string,
|
||||||
|
feedback: {
|
||||||
|
uuid: string,
|
||||||
|
rating: number
|
||||||
|
},
|
||||||
product: IProduct
|
product: IProduct
|
||||||
}
|
}
|
||||||
}[]
|
}[]
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export interface IProduct {
|
||||||
name: string,
|
name: string,
|
||||||
price: number,
|
price: number,
|
||||||
quantity: number,
|
quantity: number,
|
||||||
|
rating: number,
|
||||||
slug: string,
|
slug: string,
|
||||||
description: string,
|
description: string,
|
||||||
seoMeta: ISEOMeta,
|
seoMeta: ISEOMeta,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export * from './app/category'
|
||||||
export * from './app/store'
|
export * from './app/store'
|
||||||
export * from './app/promocodes'
|
export * from './app/promocodes'
|
||||||
export * from './app/seometa'
|
export * from './app/seometa'
|
||||||
|
export * from './app/feedbacks'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,4 +30,5 @@ export * from './api/categories'
|
||||||
export * from './api/brands'
|
export * from './api/brands'
|
||||||
export * from './api/contact'
|
export * from './api/contact'
|
||||||
export * from './api/store'
|
export * from './api/store'
|
||||||
export * from './api/promocodes'
|
export * from './api/promocodes'
|
||||||
|
export * from './api/feedbacks'
|
||||||
Loading…
Reference in a new issue