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:
Alexandr SaVBaD Waltz 2025-10-06 18:19:19 +03:00
parent 949e077942
commit c9807bd6d4
34 changed files with 418 additions and 154 deletions

View file

@ -20,7 +20,7 @@
import {useAppConfig} from "~/composables/config";
import { DEFAULT_LOCALE } from '~/config/constants';
import {useRefresh} from "~/composables/auth";
import {useLanguages} from "~/composables/languages";
import {useLanguages, useLocaleRedirect} from "~/composables/languages";
import {useCompanyInfo} from "~/composables/company";
import {useCategories} from "~/composables/categories";
@ -55,6 +55,7 @@ const cookieLocale = useCookie(
const { refresh } = useRefresh();
const { getCategories } = await useCategories();
const { isSupportedLocale } = useLocaleRedirect();
let refreshInterval: NodeJS.Timeout;
@ -83,20 +84,32 @@ watch(locale, () => {
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 () => {
refreshInterval = setInterval(async () => {
await refresh();
}, 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(
() => appStore.isOverflowHidden,
(hidden) => {

View file

@ -30,14 +30,14 @@
</template>
<script setup lang="ts">
const companyStore = useCompanyStore()
const { t } = useI18n()
const companyStore = useCompanyStore();
const { t } = useI18n();
const companyInfo = computed(() => companyStore.companyInfo)
const companyInfo = computed(() => companyStore.companyInfo);
const encodedCompanyAddress = computed(() => {
return companyInfo.value?.companyAddress ? encodeURIComponent(companyInfo.value?.companyAddress) : ''
})
return companyInfo.value?.companyAddress ? encodeURIComponent(companyInfo.value?.companyAddress) : '';
});
</script>
<style scoped lang="scss">

View file

@ -59,25 +59,25 @@ import {onClickOutside} from "@vueuse/core";
import type {ICategory} from "~/types";
import {useCategoryStore} from "~/stores/category";
const { t } = useI18n()
const { t } = useI18n();
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) => {
isBlockOpen.value = state
}
isBlockOpen.value = state;
};
// TODO: add loading state
const blockRef = ref(null)
onClickOutside(blockRef, () => setBlock(false))
const blockRef = ref(null);
onClickOutside(blockRef, () => setBlock(false));
const activeCategory = ref<ICategory>(categories.value[0]?.node)
const activeCategory = ref<ICategory>(categories.value[0]?.node);
const setActiveCategory = (category: ICategory) => {
activeCategory.value = category
}
activeCategory.value = category;
};
</script>
<style lang="scss" scoped>

View file

@ -42,19 +42,19 @@
</form>
</template>
<script setup>
<script setup lang="ts">
import {useValidators} from "~/composables/rules";
import {useContactUs} from "~/composables/contact/index.js";
const { t } = useI18n()
const { t } = useI18n();
const { required } = useValidators()
const { required } = useValidators();
const name = ref('')
const email = ref('')
const phoneNumber = ref('')
const subject = ref('')
const message = ref('')
const name = ref<string>('');
const email = ref<string>('');
const phoneNumber = ref<string>('');
const subject = ref<string>('');
const message = ref<string>('');
const isFormValid = computed(() => {
return (
@ -63,8 +63,8 @@ const isFormValid = computed(() => {
required(phoneNumber.value) === true &&
required(subject.value) === true &&
required(message.value) === true
)
})
);
});
const { contactUs, loading } = useContactUs();

View file

@ -24,28 +24,28 @@
</form>
</template>
<script setup>
<script setup lang="ts">
import {useValidators} from "~/composables/rules/index.js";
import {useNewPassword} from "@/composables/auth";
const { t } = useI18n()
const { t } = useI18n();
const { isPasswordValid } = useValidators()
const { isPasswordValid } = useValidators();
const password = ref('')
const confirmPassword = ref('')
const password = ref<string>('');
const confirmPassword = ref<string>('');
const compareStrings = (v) => {
if (v === password.value) return true
return t('errors.compare')
}
const compareStrings = (v: string) => {
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();

View file

@ -19,21 +19,21 @@
</form>
</template>
<script setup>
<script setup lang="ts">
import {useValidators} from "~/composables/rules";
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(() => {
return (
isEmail(email.value) === true
)
})
);
});
const { resetPassword, loading } = usePasswordReset();

View file

@ -9,13 +9,13 @@
</template>
<script setup lang="ts">
const img = useImage()
const img = useImage();
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>
<style lang="scss" scoped>

View file

@ -4,6 +4,7 @@ import {GET_CATEGORIES} from "~/graphql/queries/standalone/categories";
export async function useCategories() {
const categoryStore = useCategoryStore();
const { locale } = useI18n();
const getCategories = async (cursor?: string): Promise<void> => {
const {data, error} = await useAsyncQuery<ICategoriesResponse>(
@ -31,6 +32,11 @@ export async function useCategories() {
if (error.value) console.error('useCategories error:', error.value);
}
watch(locale, async () => {
categoryStore.setCategories([]);
await getCategories();
});
return {
getCategories
};

View file

@ -3,25 +3,37 @@ import type {IContactUsResponse} from "~/types";
import {CONTACT_US} from "~/graphql/mutations/contact";
import {useNotification} from "~/composables/notification";
interface IContactUsArguments {
name: string,
email: string,
phoneNumber?: string,
subject?: string,
message: string
}
export function useContactUs() {
const {t} = useI18n();
const { mutate, loading, error } = useMutation<IContactUsResponse>(CONTACT_US);
async function contactUs(
name: string,
email: string,
phoneNumber: string,
subject: string,
message: string
args: IContactUsArguments
) {
const result = await mutate({
name,
email,
phoneNumber,
subject,
message
});
const variables: Record<string, any> = {
name: args.name,
email: args.email,
message: args.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) {
useNotification({

View file

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

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

View file

@ -6,6 +6,7 @@ import {DEFAULT_LOCALE} from "@intlify/core-base";
export function useLanguageSwitch() {
const userStore = useUserStore();
const router = useRouter();
const { $apollo } = useNuxtApp() as any;
const { COOKIES_LOCALE_KEY } = useAppConfig();
const switchLocalePath = useSwitchLocalePath();
@ -23,11 +24,21 @@ export function useLanguageSwitch() {
const { mutate, loading, error } = useMutation<IUserResponse>(SWITCH_LANGUAGE);
let isSwitching = false;
async function switchLanguage(
locale: string
) {
if (isSwitching || cookieLocale.value === locale) return;
try {
isSwitching = true;
cookieLocale.value = locale;
await router.push({path: switchLocalePath(cookieLocale.value as LocaleDefinition['code'])})
await $apollo.defaultClient.clearStore();
await router.push({path: switchLocalePath(cookieLocale.value as LocaleDefinition['code'])});
if (isAuthenticated.value) {
const result = await mutate({
@ -36,9 +47,20 @@ export function useLanguageSwitch() {
});
if (result?.data?.updateUser) {
userStore.setUser(result.data.updateUser.user)
userStore.setUser(result.data.updateUser.user);
}
}
await new Promise(resolve => setTimeout(resolve, 100));
await $apollo.defaultClient.refetchQueries({
include: 'active'
});
} catch (error) {
console.error('Error switching language:', error);
} finally {
isSwitching = false;
}
}
watch(error, (err) => {

View file

@ -32,6 +32,7 @@ export function useLocaleRedirect() {
}
return {
isSupportedLocale,
checkAndRedirect
};
}

View file

@ -23,10 +23,14 @@ export function useNotification(
const messageVNode = h('div', [bodyContent, progressBar])
ElNotification({
const notification = ElNotification({
title: args.title,
duration,
duration: 0,
message: messageVNode,
type: args.type
} as NotificationOptions)
setTimeout(() => {
notification.close()
}, duration)
}

View file

@ -1,2 +1,3 @@
export * from './useOrderOverwrite';
export * from './useOrders';
export * from './useOrderBuy';

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

View file

@ -1,10 +1,13 @@
import { GET_PRODUCTS } from '~/graphql/queries/standalone/products';
import type { IProductResponse } from '~/types';
export function useProducts() {
const variables = ref({ first: 12 });
export function useProducts(params: Record<string, any> = {}) {
const variables = ref({
first: 12,
...params
});
const { data, error, refresh } = useAsyncQuery<IProductResponse>(
const { data, error } = useAsyncQuery<IProductResponse>(
GET_PRODUCTS,
variables
);
@ -12,18 +15,20 @@ export function useProducts() {
const products = computed(() => data.value?.products?.edges ?? []);
const pageInfo = computed(() => data.value?.products?.pageInfo ?? {});
const getProducts = async (params: Record<string, any> = {}) => {
variables.value = { ...variables.value, ...params };
await refresh();
};
watch(error, (e) => {
if (e) console.error('useProducts error:', e);
});
const updateVariables = (newParams: Record<string, any>) => {
variables.value = {
first: 12,
...newParams
};
};
return {
products,
pageInfo,
getProducts
updateVariables
};
}

View file

@ -36,21 +36,38 @@ export function useStore(
const products = ref([...(data.value?.products.edges ?? [])]);
const pageInfo = computed(() => data.value?.products.pageInfo ?? null);
const isInitialized = ref(false);
watch(error, e => e && console.error('useStore products error', e));
watch(
data,
(newData) => {
if (!newData?.products?.edges) return;
const newEdges = newData.products.edges;
if (!isInitialized.value || !variables.productAfter) {
products.value = [...newEdges];
isInitialized.value = true;
} else {
products.value = [...products.value, ...newEdges];
}
},
{ immediate: true }
);
watch(
() => variables.productAfter,
async (newCursor, oldCursor) => {
if (!newCursor || newCursor === oldCursor) return;
if (!newCursor || newCursor === oldCursor || !isInitialized.value) return;
await refresh();
const newEdges = data.value?.products.edges ?? [];
products.value = [...products.value, ...newEdges];
}
);
watch(
[
() => variables.categoriesSlugs,
() => variables.attributes,
() => variables.orderBy,
() => variables.minPrice,
@ -59,7 +76,6 @@ export function useStore(
async () => {
variables.productAfter = '';
await refresh();
products.value = [...(data.value?.products.edges ?? [])];
}
);

View file

@ -0,0 +1,7 @@
export const FEEDBACK_FRAGMENT = gql`
fragment Feedback on FeedbackType {
comment
rating
uuid
}
`

View file

@ -4,6 +4,7 @@ export const PRODUCT_FRAGMENT = gql`
name
price
quantity
rating
slug
description
brand {

View file

@ -84,3 +84,21 @@ export const BULK_CART = gql`
}
${ORDER_FRAGMENT}
`
export const BUY_CART = gql`
mutation buyOrder(
$orderUuid: String!,
$forcePayment: Boolean!
) {
buyOrder(
orderUuid: $orderUuid
forcePayment: $forcePayment
) {
transaction {
amount
process
paymentMethod
}
}
}
`

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

View file

@ -1,7 +1,7 @@
import {USER_FRAGMENT} from "~/graphql/fragments/user.fragment";
export const SWITCH_LANGUAGE = gql`
mutation setlanguage(
mutation setLanguage(
$uuid: UUID!,
$language: String,
) {

View file

@ -66,7 +66,8 @@
"avatarUpload": "You have successfully uploaded an avatar!",
"userUpdate": "Profile successfully updated!",
"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}!",
"failAdd": "Please log in to make a purchase",

View file

@ -44,7 +44,7 @@ export default defineNuxtConfig({
title: process.env.EVIBES_PROJECT_NAME,
titleTemplate: `${process.env.EVIBES_PROJECT_NAME} | %s`,
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: {

View file

@ -16,7 +16,6 @@ import {useUserActivation} from "~/composables/user";
import { useRouteQuery } from '@vueuse/router';
import {useBrands} from "~/composables/brands";
import {useProducts, useProductTags} from "~/composables/products";
import type {IProduct} from "~/types";
const {t} = useI18n();
const appStore = useAppStore();
@ -24,7 +23,7 @@ const route = useRoute();
useHead({
title: t('breadcrumbs.home'),
})
});
const token = useRouteQuery('token', '');
const uid = useRouteQuery('uid', '');
@ -32,17 +31,10 @@ const referrer = useRouteQuery('referrer', '');
const { activateUser } = useUserActivation();
const newProducts = ref<{ cursor: string; node: IProduct }[]>([]);
const priceProducts = ref<{ cursor: string; node: IProduct }[]>([]);
const { brands } = useBrands();
const { tags } = useProductTags();
const { products, getProducts } = useProducts();
await getProducts({ orderBy: '-modified' });
newProducts.value = products.value;
await getProducts({ orderBy: '-price' });
priceProducts.value = products.value;
const { products: newProducts } = useProducts({ orderBy: '-modified' });
const { products: priceProducts } = useProducts({ orderBy: '-price' });
onMounted( async () => {
if (route.path.includes('activate-user') && token.value && uid.value) {

View file

@ -53,7 +53,7 @@
</div>
<el-rate
class="white"
v-model="rating"
:model-value="product?.rating"
allow-half
disabled
/>
@ -224,10 +224,7 @@ if (meta) {
});
}
const { products, getProducts } = useProducts();
await getProducts({
categoriesSlugs: product.value?.category.slug
});
const { products } = useProducts({ categoriesSlugs: product.value?.category.slug });
const isProductInWishlist = computed(() => {
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 edges = product.value?.attributeGroups.edges ?? [];

View file

@ -0,0 +1,5 @@
import type {IFeedback} from "~/types";
export interface IFeedbackActionResponse {
feedback: IFeedback
}

View file

@ -41,3 +41,15 @@ export interface IBulkOrderResponse {
order: IOrder
}
}
export interface IBuyOrderResponse {
buyOrder: {
transaction: {
amount: number,
process: {
invoice_id: number,
redirect_url: string
}
}
}
}

View file

@ -0,0 +1,5 @@
export interface IFeedback {
comment: string,
rating: number,
uuid: string
}

View file

@ -15,6 +15,10 @@ export interface IOrder {
attributes: string,
quantity: number,
status: string,
feedback: {
uuid: string,
rating: number
},
product: IProduct
}
}[]

View file

@ -5,6 +5,7 @@ export interface IProduct {
name: string,
price: number,
quantity: number,
rating: number,
slug: string,
description: string,
seoMeta: ISEOMeta,

View file

@ -12,6 +12,7 @@ export * from './app/category'
export * from './app/store'
export * from './app/promocodes'
export * from './app/seometa'
export * from './app/feedbacks'
@ -30,3 +31,4 @@ export * from './api/brands'
export * from './api/contact'
export * from './api/store'
export * from './api/promocodes'
export * from './api/feedbacks'