Merge remote-tracking branch 'origin/storefront-nuxt' into storefront-nuxt

This commit is contained in:
Egor Pavlovich Gorbunov 2025-09-13 13:02:24 +03:00
commit dc19e1f0a0
67 changed files with 658 additions and 673 deletions

View file

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="evibes/settings/__init__.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/blog/templates" />
<option value="$MODULE_DIR$/core/templates" />
<option value="$MODULE_DIR$/payments/templates" />
<option value="$MODULE_DIR$/vibes_auth/templates" />
</list>
</option>
</component>
</module>

View file

@ -1,11 +0,0 @@
User-agent: *
Disallow: /admin/
Disallow: /static/
Disallow: /media/
Disallow: /cart/
Disallow: /account/
Allow: /
Sitemap: https://evibes.com/sitemap.xml
Host: evibes.com

View file

@ -1,75 +0,0 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

View file

@ -23,7 +23,6 @@ import {useRefresh} from "~/composables/auth";
import {useLanguages} from "~/composables/languages"; import {useLanguages} from "~/composables/languages";
import {useCompanyInfo} from "~/composables/company"; import {useCompanyInfo} from "~/composables/company";
import {useCategories} from "~/composables/categories"; import {useCategories} from "~/composables/categories";
import {useNotification} from "~/composables/notification";
const { locale } = useI18n(); const { locale } = useI18n();
const route = useRoute(); const route = useRoute();
@ -38,7 +37,8 @@ const showBreadcrumbs = computed(() => {
'brand', 'brand',
'search', 'search',
'profile', 'profile',
'activate-user' 'activate-user',
'reset-password'
].some(prefix => name.startsWith(prefix)); ].some(prefix => name.startsWith(prefix));
}); });

View file

@ -52,6 +52,8 @@
<nuxt-img <nuxt-img
:src="product.node.product.images.edges[0].node.image" :src="product.node.product.images.edges[0].node.image"
:alt="product.node.product.name" :alt="product.node.product.name"
format="webp"
densities="x1"
/> />
<p>{{ product.node.product.name }}</p> <p>{{ product.node.product.name }}</p>
</div> </div>

View file

@ -67,6 +67,7 @@
productUuid: product.uuid, productUuid: product.uuid,
productName: product.name productName: product.name
})" })"
:type="'button'"
:isLoading="removeLoading" :isLoading="removeLoading"
> >
{{ t('buttons.removeFromCart') }} {{ t('buttons.removeFromCart') }}
@ -79,6 +80,7 @@
productUuid: product.uuid, productUuid: product.uuid,
productName: product.name productName: product.name
})" })"
:type="'button'"
:isLoading="addLoading" :isLoading="addLoading"
> >
{{ t('buttons.addToCart') }} {{ t('buttons.addToCart') }}
@ -106,7 +108,7 @@
> >
- -
</button> </button>
<span class="tools__item tools__item-count" v-text="'X' + productinCartQuantity" /> <span class="tools__item tools__item-count" v-text="'X' + productInCartQuantity" />
<button <button
class="tools__item tools__item-button" class="tools__item tools__item-button"
@click="overwriteOrder({ @click="overwriteOrder({
@ -156,7 +158,7 @@ const isProductInWishlist = computed(() => {
const isProductInCart = computed(() => { const isProductInCart = computed(() => {
return cartStore.currentOrder?.orderProducts?.edges.find((prod) => prod.node.product.uuid === props.product?.uuid); return cartStore.currentOrder?.orderProducts?.edges.find((prod) => prod.node.product.uuid === props.product?.uuid);
}); });
const productinCartQuantity = computed(() => { const productInCartQuantity = computed(() => {
return cartStore.currentOrder?.orderProducts?.edges.filter((prod) => prod.node.product.uuid === props.product.uuid)[0].node.quantity; return cartStore.currentOrder?.orderProducts?.edges.filter((prod) => prod.node.product.uuid === props.product.uuid)[0].node.quantity;
}); });

View file

@ -32,6 +32,7 @@
v-model="message" v-model="message"
/> />
<ui-button <ui-button
:type="'submit'"
class="form__button" class="form__button"
:isDisabled="!isFormValid" :isDisabled="!isFormValid"
:isLoading="loading" :isLoading="loading"

View file

@ -18,6 +18,7 @@
/> />
</div> </div>
<ui-button <ui-button
:type="'submit'"
class="form__button" class="form__button"
:isDisabled="!isFormValid" :isDisabled="!isFormValid"
:isLoading="loading" :isLoading="loading"

View file

@ -25,6 +25,7 @@
{{ t('forms.login.forgot') }} {{ t('forms.login.forgot') }}
</ui-link> </ui-link>
<ui-button <ui-button
:type="'submit'"
class="form__button" class="form__button"
:isDisabled="!isFormValid" :isDisabled="!isFormValid"
:isLoading="loading" :isLoading="loading"

View file

@ -14,6 +14,7 @@
v-model="confirmPassword" v-model="confirmPassword"
/> />
<ui-button <ui-button
:type="'submit'"
class="form__button" class="form__button"
:isDisabled="!isFormValid" :isDisabled="!isFormValid"
:isLoading="loading" :isLoading="loading"

View file

@ -44,6 +44,7 @@
v-model="confirmPassword" v-model="confirmPassword"
/> />
<ui-button <ui-button
:type="'submit'"
class="form__button" class="form__button"
:isDisabled="!isFormValid" :isDisabled="!isFormValid"
:isLoading="loading" :isLoading="loading"

View file

@ -9,6 +9,7 @@
:inputMode="'email'" :inputMode="'email'"
/> />
<ui-button <ui-button
:type="'submit'"
class="form__button" class="form__button"
:isDisabled="!isFormValid" :isDisabled="!isFormValid"
:isLoading="loading" :isLoading="loading"

View file

@ -43,6 +43,7 @@
/> />
</div> </div>
<ui-button <ui-button
:type="'submit'"
class="form__button" class="form__button"
:isLoading="loading" :isLoading="loading"
> >

View file

@ -1,20 +1,19 @@
<template> <template>
<div class="brands"> <div class="brands">
<client-only> <nuxt-marquee
<NuxtMarquee
class="brand__marquee" class="brand__marquee"
id="marquee-slider" id="marquee-slider"
:speed="50" :speed="50"
:pauseOnHover="true" :pauseOnHover="true"
> >
<div <nuxt-link-locale
class="brands__item"
v-for="brand in brands" v-for="brand in brands"
:key="brand.node.uuid" :key="brand.node.uuid"
:to="`/brand/${brand.node.slug}`"
> >
<nuxt-link-locale <div
:to="`/brand/${brand.node.uuid}`" class="brands__item"
> >
<nuxt-img <nuxt-img
densities="x1" densities="x1"
@ -23,17 +22,18 @@
loading="lazy" loading="lazy"
class="brands__item-image" class="brands__item-image"
/> />
</nuxt-link-locale>
</div> </div>
</NuxtMarquee> </nuxt-link-locale>
</client-only> </nuxt-marquee>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {useBrands} from "~/composables/brands"; import type {IBrand} from "~/types";
const { brands } = await useBrands(); defineProps<{
brands: { node: IBrand }[]
}>()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,5 +1,8 @@
<template> <template>
<div class="tags"> <div
class="tags"
v-if="tags.length > 0"
>
<home-category-tags-block <home-category-tags-block
v-for="tag in tags" v-for="tag in tags"
:key="tag.node.uuid" :key="tag.node.uuid"

View file

@ -9,6 +9,7 @@
:key="tag.node.uuid" :key="tag.node.uuid"
:tag="tag.node" :tag="tag.node"
/> />
<client-only>
<home-collection-inner <home-collection-inner
v-if="newProducts.length > 0" v-if="newProducts.length > 0"
:tag="newProductsTag" :tag="newProductsTag"
@ -17,6 +18,7 @@
v-if="priceProducts.length > 0" v-if="priceProducts.length > 0"
:tag="priceProductsTag" :tag="priceProductsTag"
/> />
</client-only>
</div> </div>
</div> </div>
</div> </div>
@ -24,20 +26,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {useProducts, useProductTags} from "@/composables/products"; import type {IProduct, IProductTag} from "~/types";
const { t } = useI18n() const props = defineProps<{
tags: { node: IProductTag }[],
newProducts: { cursor: string; node: IProduct }[],
priceProducts: { cursor: string; node: IProduct }[]
}>()
const { tags } = await useProductTags(); const { t } = useI18n();
const {
products: newProducts,
getProducts: getNewProducts
} = await useProducts();
const {
products: priceProducts,
getProducts: getPriceProducts
} = await useProducts();
const newProductsTag = computed(() => { const newProductsTag = computed(() => {
return { return {
@ -45,7 +42,7 @@ const newProductsTag = computed(() => {
tagName: t('home.collection.newTag'), tagName: t('home.collection.newTag'),
uuid: 'new-products', uuid: 'new-products',
productSet: { productSet: {
edges: newProducts.value edges: props.newProducts
} }
} }
}); });
@ -56,19 +53,10 @@ const priceProductsTag = computed(() => {
tagName: t('home.collection.cheapTag'), tagName: t('home.collection.cheapTag'),
uuid: 'price-products', uuid: 'price-products',
productSet: { productSet: {
edges: priceProducts.value edges: props.priceProducts
} }
} }
}); });
await Promise.all([
getNewProducts({
orderBy: '-modified'
}),
getPriceProducts({
orderBy: '-price'
})
]);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -55,6 +55,7 @@
{{ t('store.filters.reset') }} {{ t('store.filters.reset') }}
</button> </button>
<ui-button <ui-button
:type="'button'"
@click="onApply" @click="onApply"
> >
{{ t('store.filters.apply') }} {{ t('store.filters.apply') }}

View file

@ -10,7 +10,9 @@
<store-top <store-top
v-model="orderBy" v-model="orderBy"
@toggle-filter="onFilterToggle" @toggle-filter="onFilterToggle"
:isFiltersVisible="filters.length > 0"
/> />
<client-only>
<div <div
class="store__list" class="store__list"
:class="[ :class="[
@ -19,7 +21,7 @@
]" ]"
> >
<cards-product <cards-product
v-if="products.length" v-if="products.length && !pending"
v-for="product in products" v-for="product in products"
:key="product.node.uuid" :key="product.node.uuid"
:product="product.node" :product="product.node"
@ -32,6 +34,7 @@
:isList="productView === 'list'" :isList="productView === 'list'"
/> />
</div> </div>
</client-only>
<div class="store__list-observer" ref="observer"></div> <div class="store__list-observer" ref="observer"></div>
</div> </div>
</template> </template>
@ -42,8 +45,11 @@ import {useRouteQuery} from "@vueuse/router";
import {useRouteParams} from "@vueuse/router"; import {useRouteParams} from "@vueuse/router";
import {useCategoryBySlug} from "~/composables/categories"; import {useCategoryBySlug} from "~/composables/categories";
import {useAppConfig} from '~/composables/config'; import {useAppConfig} from '~/composables/config';
import {useDefaultSeo} from "~/composables/seo";
const { COOKIES_PRODUCT_VIEW_KEY } = useAppConfig(); const { locale } = useI18n();
const { COOKIES_PRODUCT_VIEW_KEY, APP_NAME } = useAppConfig();
const productView = useCookie<string>( const productView = useCookie<string>(
COOKIES_PRODUCT_VIEW_KEY as string, COOKIES_PRODUCT_VIEW_KEY as string,
{ {
@ -59,7 +65,39 @@ const minPrice = useRouteQuery<number>('minPrice', 0);
const maxPrice = useRouteQuery<number>('maxPrice', 50000); const maxPrice = useRouteQuery<number>('maxPrice', 50000);
const observer = ref(null); const observer = ref(null);
const { category, filters } = await useCategoryBySlug(slug.value); const { category, seoMeta, filters } = await useCategoryBySlug(slug.value);
const meta = useDefaultSeo(seoMeta.value || null);
if (meta) {
useSeoMeta({
title: meta.title || APP_NAME,
description: meta.description || meta.title || APP_NAME,
ogTitle: meta.og.title || undefined,
ogDescription: meta.og.description || meta.title || APP_NAME,
ogType: meta.og.type || undefined,
ogUrl: meta.og.url || undefined,
ogImage: meta.og.image || undefined,
twitterCard: meta.twitter.card || undefined,
twitterTitle: meta.twitter.title || undefined,
twitterDescription: meta.twitter.description || undefined,
robots: meta.robots,
});
useHead({
link: [
meta.canonical ? { rel: 'canonical', href: meta.canonical } : {},
].filter(Boolean) as any,
meta: [{ property: 'og:locale', content: locale.value }],
script: meta.jsonLd.map((obj: any) => ({
type: 'application/ld+json',
innerHTML: JSON.stringify(obj),
})),
__dangerouslyDisableSanitizersByTagID: Object.fromEntries(
meta.jsonLd.map((_, i: number) => [`ldjson-${i}`, ['innerHTML']])
),
});
}
watch( watch(
() => category.value, () => category.value,
@ -71,7 +109,7 @@ watch(
{ immediate: true } { immediate: true }
); );
const { pending, products, pageInfo, variables } = await useStore( const { pending, products, pageInfo, variables } = useStore(
slug.value, slug.value,
attributes.value, attributes.value,
orderBy.value, orderBy.value,

View file

@ -35,7 +35,7 @@
</button> </button>
</div> </div>
</div> </div>
<div class="top__filter"> <div class="top__filter" v-if="isFiltersVisible">
<button <button
class="top__filter-button" class="top__filter-button"
@click="$emit('toggle-filter')" @click="$emit('toggle-filter')"
@ -53,6 +53,7 @@ import { useAppConfig } from '~/composables/config';
const props = defineProps<{ const props = defineProps<{
modelValue: string modelValue: string
isFiltersVisible: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: string): void (e: 'update:modelValue', value: string): void

View file

@ -3,7 +3,7 @@
class="button" class="button"
:disabled="isDisabled" :disabled="isDisabled"
:class="[{active: isLoading}]" :class="[{active: isLoading}]"
type="submit" :type="type"
> >
<ui-loader class="button__loader" v-if="isLoading" /> <ui-loader class="button__loader" v-if="isLoading" />
<slot v-else /> <slot v-else />
@ -12,6 +12,7 @@
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ const props = defineProps<{
type: 'submit' | 'button',
isDisabled?: boolean, isDisabled?: boolean,
isLoading?: boolean isLoading?: boolean
}>() }>()

View file

@ -1,22 +1,23 @@
<template> <template>
<div @click="redirect" class="link"> <div
v-on="props.routePath ? { click: redirect } : {}"
class="link"
>
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ const props = defineProps<{
routePath: string routePath?: string
}>(); }>();
const router = useRouter(); const router = useRouter();
const redirect = () => { const redirect = () => {
if (props.routePath) {
router.push({ router.push({
path: props.routePath path: props.routePath
}); });
}
}; };
</script> </script>

View file

@ -57,8 +57,6 @@ export function useLogin() {
userStore.setUser(authData.user); userStore.setUser(authData.user);
cookieAccess.value = authData.accessToken; cookieAccess.value = authData.accessToken;
await nextTick();
appStore.unsetActiveState(); appStore.unsetActiveState();
useNotification({ useNotification({

View file

@ -64,8 +64,6 @@ export function useRefresh() {
// }); // });
// await usePromocodes(); // await usePromocodes();
await nextTick();
await useUserBaseData(data.user.email); await useUserBaseData(data.user.email);
} }

View file

@ -1,2 +1,2 @@
export * from './useBrands'; export * from './useBrands';
export * from './useBrandByUuid'; export * from './useBrandBySlug';

View file

@ -0,0 +1,22 @@
import {GET_BRAND_BY_SLUG} from "~/graphql/queries/standalone/brands";
import type {IBrandsResponse} from "~/types";
export async function useBrandBySlug(slug: string) {
const brand = computed(() => data.value?.brands.edges[0]?.node ?? null);
const { data, error } = await useAsyncQuery<IBrandsResponse>(
GET_BRAND_BY_SLUG,
{ slug }
);
watch(error, (err) => {
if (err) {
console.error('useBrandsBySlug error:', err)
}
});
return {
brand,
seoMeta: computed(() => brand.value?.seoMeta)
}
}

View file

@ -1,21 +0,0 @@
import {GET_BRAND_BY_UUID} from "~/graphql/queries/standalone/brands";
import type {IBrandsResponse} from "~/types";
export async function useBrandByUuid(uuid: string) {
const brand = computed(() => data.value?.brands.edges[0].node ?? []);
const { data, error } = await useAsyncQuery<IBrandsResponse>(
GET_BRAND_BY_UUID,
{ uuid }
);
watch(error, (err) => {
if (err) {
console.error('useBrandsByUuid error:', err)
}
});
return {
brand
}
}

View file

@ -1,13 +1,13 @@
import {GET_BRANDS} from "~/graphql/queries/standalone/brands"; import {GET_BRANDS} from "~/graphql/queries/standalone/brands";
import type {IBrandsResponse} from "~/types"; import type {IBrandsResponse} from "~/types";
export async function useBrands() { export function useBrands() {
const brands = computed(() => data.value?.brands.edges ?? []); const { data, error } = useAsyncQuery<IBrandsResponse>(
const { data, error } = await useAsyncQuery<IBrandsResponse>(
GET_BRANDS GET_BRANDS
); );
const brands = computed(() => data.value?.brands.edges ?? []);
watch(error, (err) => { watch(error, (err) => {
if (err) { if (err) {
console.error('useBrands error:', err) console.error('useBrands error:', err)

View file

@ -22,6 +22,7 @@ export async function useCategoryBySlug(slug: string) {
return { return {
category, category,
seoMeta: computed(() => category.value?.seoMeta),
filters filters
}; };
} }

View file

@ -0,0 +1,21 @@
import type {ICategoryBySlugSeoResponse} from '~/types';
import {GET_CATEGORY_BY_SLUG_SEO} from "~/graphql/queries/standalone/categories";
export async function useCategoryBySlugSeo(slug: string) {
const category = computed(() => data.value?.categories.edges[0]?.node ?? null);
const { data, error } = await useAsyncQuery<ICategoryBySlugSeoResponse>(
GET_CATEGORY_BY_SLUG_SEO,
{ categorySlug: slug }
);
watch(error, (err) => {
if (err) {
console.error('useCategoryBySlugSeo error:', err)
}
});
return {
seoMeta: computed(() => category.value?.seoMeta)
};
}

View file

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

View file

@ -0,0 +1,38 @@
import type {IPostResponse} from '~/types';
import {GET_POST_BY_SLUG} from "~/graphql/queries/standalone/blog";
export async function usePostBySlug(slug: string) {
const { locale } = useI18n();
const variables = reactive({
slug: slug,
first: 1
});
const { data, error, refresh } = await useAsyncQuery<IPostResponse>(
GET_POST_BY_SLUG,
variables
);
const post = computed(() => data.value?.posts.edges[0]?.node ?? null);
watch(locale, async () => {
if (variables.first >= 100) {
variables.first = 0;
} else {
variables.first += 1;
}
await refresh();
});
watch(error, (err) => {
if (err) {
console.error('usePostBySlug error:', err)
}
});
return {
post,
refresh
};
}

View file

@ -21,6 +21,7 @@ export async function useProductBySlug(slug: string) {
}); });
return { return {
product product,
seoMeta: computed(() => product.value?.seoMeta)
}; };
} }

View file

@ -1,13 +1,13 @@
import {GET_PRODUCT_TAGS} from "~/graphql/queries/standalone/products.js"; import {GET_PRODUCT_TAGS} from "~/graphql/queries/standalone/products.js";
import type {IProductTagsResponse} from "~/types"; import type {IProductTagsResponse} from "~/types";
export async function useProductTags() { export function useProductTags() {
const tags = computed(() => data.value?.productTags?.edges ?? []); const { data, error } = useAsyncQuery<IProductTagsResponse>(
const { data, error } = await useAsyncQuery<IProductTagsResponse>(
GET_PRODUCT_TAGS GET_PRODUCT_TAGS
); );
const tags = computed(() => data.value?.productTags?.edges ?? []);
watch(error, (err) => { watch(error, (err) => {
if (err) { if (err) {
console.error('useProductTags error:', err) console.error('useProductTags error:', err)

View file

@ -1,10 +1,10 @@
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 async function useProducts() { export function useProducts() {
const variables = ref({ first: 12 }); const variables = ref({ first: 12 });
const { data, error, refresh } = await useAsyncQuery<IProductResponse>( const { data, error, refresh } = useAsyncQuery<IProductResponse>(
GET_PRODUCTS, GET_PRODUCTS,
variables variables
); );

View file

@ -2,7 +2,7 @@ import type {IPromocodesResponse} from "~/types";
import {GET_PROMOCODES} from "~/graphql/queries/standalone/promocodes"; import {GET_PROMOCODES} from "~/graphql/queries/standalone/promocodes";
export async function usePromocodes () { export async function usePromocodes () {
const promocodesStore = usePromocodeStore(); const promocodeStore = usePromocodeStore();
const { data, error } = await useAsyncQuery<IPromocodesResponse>( const { data, error } = await useAsyncQuery<IPromocodesResponse>(
GET_PROMOCODES GET_PROMOCODES
@ -10,7 +10,7 @@ export async function usePromocodes () {
console.log(data.value) console.log(data.value)
if (!error.value && data.value?.promocodes.edges) { if (!error.value && data.value?.promocodes.edges) {
promocodesStore.setPromocodes(data.value.promocodes.edges); promocodeStore.setPromocodes(data.value.promocodes.edges);
} }
watch(error, (err) => { watch(error, (err) => {

View file

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

View file

@ -0,0 +1,27 @@
import type {ISEOMeta} from "~/types";
export function useDefaultSeo (meta: ISEOMeta | null) {
if (meta === null) return meta
const norm = (s?: string) => (s ?? '').toString().trim()
return {
title: norm(meta.title),
description: norm(meta.description),
canonical: norm(meta.canonical),
robots: norm(meta.robots || 'index,follow'),
og: {
title: norm(meta.openGraph?.title || meta.title),
description: norm(meta.openGraph?.description || meta.description),
type: norm(meta.openGraph?.type || 'product'),
url: norm(meta.openGraph?.url || meta.canonical),
image: norm(meta.openGraph?.image),
},
twitter: {
card: norm(meta.twitter?.card || 'summary_large_image'),
title: norm(meta.twitter?.title || meta.title),
description: norm(meta.twitter?.description || meta.description),
},
jsonLd: Array.isArray(meta.jsonLd) ? meta.jsonLd : [],
}
}

View file

@ -11,7 +11,7 @@ interface IProdVars {
productAfter?: string productAfter?: string
} }
export async function useStore( export function useStore(
slug: string, slug: string,
attributes?: string, attributes?: string,
orderBy?: string, orderBy?: string,
@ -29,12 +29,12 @@ export async function useStore(
productAfter productAfter
}); });
const { pending, data, error, refresh } = await useAsyncQuery<IProductResponse>( const { pending, data, error, refresh } = useAsyncQuery<IProductResponse>(
GET_PRODUCTS, GET_PRODUCTS,
variables variables
); );
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);
watch(error, e => e && console.error('useStore products error', e)); watch(error, e => e && console.error('useStore products error', e));
@ -45,7 +45,7 @@ export async function useStore(
if (!newCursor || newCursor === oldCursor) return; if (!newCursor || newCursor === oldCursor) return;
await refresh(); await refresh();
const newEdges = data.value?.products.edges ?? []; const newEdges = data.value?.products.edges ?? [];
products.value.push(...newEdges); products.value = [...products.value, ...newEdges];
} }
); );
@ -59,7 +59,7 @@ export async function useStore(
async () => { async () => {
variables.productAfter = ''; variables.productAfter = '';
await refresh(); await refresh();
products.value = data.value?.products.edges ?? []; products.value = [...(data.value?.products.edges ?? [])];
} }
); );

View file

@ -15,7 +15,11 @@ export function useDeposit() {
); );
if (result?.data?.deposit) { if (result?.data?.deposit) {
window.open(result?.data.deposit.transaction.process.url) if (result?.data.deposit?.transaction?.process?.url) {
window.location.href = result?.data.deposit.transaction.process.url
} else {
console.log(result?.data)
}
} }
} }

View file

@ -5,7 +5,7 @@ import {orderStatuses} from "~/config/constants";
export async function useUserBaseData(userEmail: string) { export async function useUserBaseData(userEmail: string) {
const wishlistStore = useWishlistStore(); const wishlistStore = useWishlistStore();
const cartStore = useCartStore(); const cartStore = useCartStore();
const promocodesStore = usePromocodeStore(); const promocodeStore = usePromocodeStore();
const { document, variables } = getUserBaseData( const { document, variables } = getUserBaseData(
{ {
@ -14,19 +14,19 @@ export async function useUserBaseData(userEmail: string) {
}, },
); );
const { data, error } = await useAsyncQuery<IUserBaseDataResponse>( const { mutate, error } = useMutation<IUserBaseDataResponse>(document);
document,
variables
);
if (data.value?.wishlists.edges) { const result = await mutate(variables);
wishlistStore.setWishlist(data.value.wishlists.edges[0].node); const data = result?.data;
if (data?.wishlists.edges) {
wishlistStore.setWishlist(data.wishlists.edges[0].node);
} }
if (data.value?.orders.edges) { if (data?.orders.edges) {
cartStore.setCurrentOrders(data.value?.orders.edges[0].node); cartStore.setCurrentOrders(data.orders.edges[0].node);
} }
if (data.value?.promocodes.edges) { if (data?.promocodes.edges) {
promocodesStore.setPromocodes(data.value.promocodes.edges); promocodeStore.setPromocodes(data.promocodes.edges);
} }
watch(error, (err) => { watch(error, (err) => {

View file

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

View file

@ -0,0 +1,12 @@
export const SEOMETA_FRAGMENT = gql`
fragment SEOMeta on SEOMetaType {
canonical
description
hreflang
jsonLd
openGraph
robots
title
twitter
}
`

View file

@ -1,4 +1,4 @@
import {ORDER_FRAGMENT} from "@/graphql/fragments/orders.fragment.js"; import {ORDER_FRAGMENT} from "@/graphql/fragments/orders.fragment";
export const ADD_TO_CART = gql` export const ADD_TO_CART = gql`
mutation addToCart( mutation addToCart(

View file

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

View file

@ -1,4 +1,4 @@
import {WISHLIST_FRAGMENT} from "@/graphql/fragments/wishlist.fragment.js"; import {WISHLIST_FRAGMENT} from "@/graphql/fragments/wishlist.fragment";
export const ADD_TO_WISHLIST = gql` export const ADD_TO_WISHLIST = gql`
mutation addToWishlist( mutation addToWishlist(

View file

@ -12,10 +12,12 @@ export const GET_POSTS = gql`
export const GET_POST_BY_SLUG = gql` export const GET_POST_BY_SLUG = gql`
query getPostBySlug( query getPostBySlug(
$slug: String! $slug: String!,
$first: Int
) { ) {
posts( posts(
slug: $slug slug: $slug,
first: $first,
) { ) {
edges { edges {
node { node {

View file

@ -1,5 +1,6 @@
import {BRAND_FRAGMENT} from "@/graphql/fragments/brands.fragment.js"; import {BRAND_FRAGMENT} from "@/graphql/fragments/brands.fragment";
import {CATEGORY_FRAGMENT} from "@/graphql/fragments/categories.fragment.js"; import {CATEGORY_FRAGMENT} from "@/graphql/fragments/categories.fragment";
import {SEOMETA_FRAGMENT} from "@/graphql/fragments/seometa.fragment";
export const GET_BRANDS = gql` export const GET_BRANDS = gql`
query getBrands ( query getBrands (
@ -18,12 +19,12 @@ export const GET_BRANDS = gql`
${BRAND_FRAGMENT} ${BRAND_FRAGMENT}
` `
export const GET_BRAND_BY_UUID = gql` export const GET_BRAND_BY_SLUG = gql`
query getBrandbyUuid( query getBrandBySlug(
$uuid: UUID! $slug: String!
) { ) {
brands( brands(
uuid: $uuid slug: $slug
) { ) {
edges { edges {
node { node {
@ -31,10 +32,14 @@ export const GET_BRAND_BY_UUID = gql`
categories { categories {
...Category ...Category
} }
seoMeta {
...SEOMeta
}
} }
} }
} }
} }
${BRAND_FRAGMENT} ${BRAND_FRAGMENT}
${CATEGORY_FRAGMENT} ${CATEGORY_FRAGMENT}
${SEOMETA_FRAGMENT}
` `

View file

@ -1,4 +1,5 @@
import {CATEGORY_FRAGMENT} from "~/graphql/fragments/categories.fragment.js"; import {CATEGORY_FRAGMENT} from "~/graphql/fragments/categories.fragment";
import {SEOMETA_FRAGMENT} from "~/graphql/fragments/seometa.fragment";
export const GET_CATEGORIES = gql` export const GET_CATEGORIES = gql`
query getCategories ( query getCategories (
@ -55,11 +56,34 @@ export const GET_CATEGORY_BY_SLUG = gql`
maxPrice maxPrice
minPrice minPrice
} }
seoMeta {
...SEOMeta
}
} }
} }
} }
} }
${CATEGORY_FRAGMENT} ${CATEGORY_FRAGMENT}
${SEOMETA_FRAGMENT}
`
export const GET_CATEGORY_BY_SLUG_SEO = gql`
query getCategoryBySlug(
$categorySlug: String!
) {
categories(
slug: $categorySlug
) {
edges {
node {
seoMeta {
...SEOMeta
}
}
}
}
}
${SEOMETA_FRAGMENT}
` `
export const GET_CATEGORY_TAGS = gql` export const GET_CATEGORY_TAGS = gql`

View file

@ -1,4 +1,4 @@
import {ORDER_FRAGMENT} from "@/graphql/fragments/orders.fragment.js"; import {ORDER_FRAGMENT} from "@/graphql/fragments/orders.fragment";
export const GET_ORDERS = gql` export const GET_ORDERS = gql`
query getOrders( query getOrders(

View file

@ -1,4 +1,5 @@
import {PRODUCT_FRAGMENT} from "@/graphql/fragments/products.fragment.js"; import {PRODUCT_FRAGMENT} from "@/graphql/fragments/products.fragment";
import {SEOMETA_FRAGMENT} from "~/graphql/fragments/seometa.fragment";
export const GET_PRODUCTS = gql` export const GET_PRODUCTS = gql`
query getProducts( query getProducts(
@ -46,11 +47,15 @@ export const GET_PRODUCT_BY_SLUG = gql`
edges { edges {
node { node {
...Product ...Product
seoMeta {
...SEOMeta
}
} }
} }
} }
} }
${PRODUCT_FRAGMENT} ${PRODUCT_FRAGMENT}
${SEOMETA_FRAGMENT}
` `
export const GET_PRODUCT_TAGS = gql` export const GET_PRODUCT_TAGS = gql`

View file

@ -1,4 +1,4 @@
import {WISHLIST_FRAGMENT} from "@/graphql/fragments/wishlist.fragment.js"; import {WISHLIST_FRAGMENT} from "@/graphql/fragments/wishlist.fragment";
export const GET_WISHLIST = gql` export const GET_WISHLIST = gql`
query getWishlist { query getWishlist {

View file

@ -203,5 +203,8 @@
"empty": "You don't have any promocodes." "empty": "You don't have any promocodes."
}, },
"logout": "Logout" "logout": "Logout"
},
"catalog": {
"title": "Catalog"
} }
} }

View file

@ -1439,17 +1439,6 @@
"integrity": "sha512-4wMPu9iN3/HL97QblBsBay3E1etIciR84izI3U+4iALY+JHCrI+a2jO0qbAZ/nxKoegypYEaiiqWXylm+/zfrw==", "integrity": "sha512-4wMPu9iN3/HL97QblBsBay3E1etIciR84izI3U+4iALY+JHCrI+a2jO0qbAZ/nxKoegypYEaiiqWXylm+/zfrw==",
"license": "Apache 2" "license": "Apache 2"
}, },
"node_modules/@netlify/blobs": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/@netlify/blobs/-/blobs-8.2.0.tgz",
"integrity": "sha512-9djLZHBKsoKk8XCgwWSEPK9QnT8qqxEQGuYh48gFIcNLvpBKkLnHbDZuyUxmNemCfDz7h0HnMXgSPnnUVgARhg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": "^14.16.0 || >=16.0.0"
}
},
"node_modules/@netlify/dev-utils": { "node_modules/@netlify/dev-utils": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@netlify/dev-utils/-/dev-utils-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@netlify/dev-utils/-/dev-utils-2.2.0.tgz",
@ -4351,14 +4340,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/change-case": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -7876,257 +7857,6 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lightningcss": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"license": "MPL-2.0",
"optional": true,
"peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-darwin-x64": "1.30.1",
"lightningcss-freebsd-x64": "1.30.1",
"lightningcss-linux-arm-gnueabihf": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-arm64-musl": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1",
"lightningcss-linux-x64-musl": "1.30.1",
"lightningcss-win32-arm64-msvc": "1.30.1",
"lightningcss-win32-x64-msvc": "1.30.1"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss/node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=8"
}
},
"node_modules/lilconfig": { "node_modules/lilconfig": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",

View file

@ -0,0 +1,76 @@
<template>
<div class="brand">
<ui-title>{{ brand?.name }}</ui-title>
<div class="brand__categories">
<cards-category
v-for="category in brand?.categories"
:key="category.uuid"
:category="category"
/>
</div>
</div>
</template>
<script setup lang="ts">
import {useBrandBySlug} from "~/composables/brands";
import {usePageTitle} from "~/composables/utils";
import {useDefaultSeo} from "~/composables/seo/index.js";
import {useRouteParams} from "@vueuse/router";
import {useAppConfig} from "~/composables/config";
const { locale } = useI18n();
const slug = useRouteParams<string>('slug');
const { APP_NAME } = useAppConfig();
const { setPageTitle } = usePageTitle();
const { brand, seoMeta } = await useBrandBySlug(slug.value);
const meta = useDefaultSeo(seoMeta.value || null);
if (meta) {
useSeoMeta({
title: meta.title || APP_NAME,
description: meta.description || meta.title || APP_NAME,
ogTitle: meta.og.title || undefined,
ogDescription: meta.og.description || meta.title || APP_NAME,
ogType: meta.og.type || undefined,
ogUrl: meta.og.url || undefined,
ogImage: meta.og.image || undefined,
twitterCard: meta.twitter.card || undefined,
twitterTitle: meta.twitter.title || undefined,
twitterDescription: meta.twitter.description || undefined,
robots: meta.robots,
});
useHead({
link: [
meta.canonical ? { rel: 'canonical', href: meta.canonical } : {},
].filter(Boolean) as any,
meta: [{ property: 'og:locale', content: locale.value }],
script: meta.jsonLd.map((obj: any) => ({
type: 'application/ld+json',
innerHTML: JSON.stringify(obj),
})),
__dangerouslyDisableSanitizersByTagID: Object.fromEntries(
meta.jsonLd.map((_, i: number) => [`ldjson-${i}`, ['innerHTML']])
),
});
}
setPageTitle(brand.value?.name ?? 'Brand');
// TODO: add product by this brand
</script>
<style lang="scss" scoped>
.brand {
&__categories {
display: grid;
grid-template-columns: repeat(auto-fill, 275px);
align-items: center;
justify-content: center;
gap: 50px;
}
}
</style>

View file

@ -1,40 +0,0 @@
<template>
<div class="brand">
<ui-title>{{ brand.name }}</ui-title>
<div class="brand__categories">
<cards-category
v-for="category in brand.categories"
:key="category.uuid"
:category="category"
/>
</div>
</div>
</template>
<script setup>
import {useBrandByUuid} from "~/composables/brands";
import {usePageTitle} from "~/composables/utils";
const route = useRoute()
const slug = computed(() => route.params.uuid)
const { setPageTitle } = usePageTitle();
const { brand } = await useBrandByUuid(slug.value);
setPageTitle(brand.value?.name ?? 'Brand');
// TODO: add product by this brand
</script>
<style lang="scss" scoped>
.brand {
&__categories {
display: grid;
grid-template-columns: repeat(auto-fill, 275px);
align-items: center;
justify-content: center;
gap: 50px;
}
}
</style>

View file

@ -19,25 +19,64 @@
<script setup lang="ts"> <script setup lang="ts">
import type {ICategory} from "~/types"; import type {ICategory} from "~/types";
import {usePageTitle} from "~/composables/utils"; import {usePageTitle} from "~/composables/utils";
import {useRouteParams} from "@vueuse/router";
import {useDefaultSeo} from "~/composables/seo";
import {useCategoryBySlugSeo} from "~/composables/categories/useCategoryBySlugSeo";
import {useAppConfig} from "~/composables/config";
const route = useRoute() const { locale } = useI18n();
const categoryStore = useCategoryStore() const categoryStore = useCategoryStore();
const { setPageTitle } = usePageTitle() const { APP_NAME } = useAppConfig();
const { setPageTitle } = usePageTitle();
const slug = computed(() => route.params.slug as string) const slug = useRouteParams<string>('slug');
const roots = computed(() => categoryStore.categories.map(e => e.node)) const roots = computed(() => categoryStore.categories.map(e => e.node));
const category = computed(() => findBySlug(roots.value, slug.value)) const category = computed(() => findBySlug(roots.value, slug.value));
setPageTitle(category.value?.name ?? 'Category') const seoMeta = useCategoryBySlugSeo(category.value.slug)
const meta = useDefaultSeo(seoMeta || null);
if (meta) {
useSeoMeta({
title: meta.title || APP_NAME,
description: meta.description || meta.title || APP_NAME,
ogTitle: meta.og.title || undefined,
ogDescription: meta.og.description || meta.title || APP_NAME,
ogType: meta.og.type || undefined,
ogUrl: meta.og.url || undefined,
ogImage: meta.og.image || undefined,
twitterCard: meta.twitter.card || undefined,
twitterTitle: meta.twitter.title || undefined,
twitterDescription: meta.twitter.description || undefined,
robots: meta.robots,
});
useHead({
link: [
meta.canonical ? { rel: 'canonical', href: meta.canonical } : {},
].filter(Boolean) as any,
meta: [{ property: 'og:locale', content: locale.value }],
script: meta.jsonLd.map((obj: any) => ({
type: 'application/ld+json',
innerHTML: JSON.stringify(obj),
})),
__dangerouslyDisableSanitizersByTagID: Object.fromEntries(
meta.jsonLd.map((_, i: number) => [`ldjson-${i}`, ['innerHTML']])
),
});
}
setPageTitle(category.value?.name ?? 'Category');
function findBySlug(nodes: ICategory[], slug: string): ICategory | undefined { function findBySlug(nodes: ICategory[], slug: string): ICategory | undefined {
for (const n of nodes) { for (const n of nodes) {
if (n.slug === slug) return n if (n.slug === slug) return n;
if (n.children?.length) { if (n.children?.length) {
const found = findBySlug(n.children, slug) const found = findBySlug(n.children, slug);
if (found) return found if (found) return found;
} }
} }
} }

View file

@ -1,8 +1,12 @@
<template> <template>
<div class="home"> <div class="home">
<home-hero /> <home-hero />
<home-brands /> <home-brands :brands="brands" />
<home-collection /> <home-collection
:tags="tags"
:newProducts="newProducts"
:priceProducts="priceProducts"
/>
<home-category-tags /> <home-category-tags />
</div> </div>
</template> </template>
@ -10,6 +14,9 @@
<script setup lang="ts"> <script setup lang="ts">
import {useUserActivation} from "~/composables/user"; import {useUserActivation} from "~/composables/user";
import { useRouteQuery } from '@vueuse/router'; 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 {t} = useI18n();
const appStore = useAppStore(); const appStore = useAppStore();
@ -25,6 +32,18 @@ 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 { tags } = useProductTags();
const { products, getProducts } = useProducts();
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) {
await activateUser(token.value, uid.value); await activateUser(token.value, uid.value);

View file

@ -1,8 +1,8 @@
<template> <template>
<div class="product" v-if="product"> <div class="product">
<div class="container"> <div class="container">
<div class="product__wrapper"> <div class="product__wrapper">
<h1 class="product__title">{{ product.name }}</h1> <h1 class="product__title">{{ product?.name }}</h1>
<div class="product__block"> <div class="product__block">
<div class="product__images"> <div class="product__images">
<div class="product__images-gallery"> <div class="product__images-gallery">
@ -14,7 +14,7 @@
> >
<nuxt-img <nuxt-img
:src="image" :src="image"
:alt="product.name" :alt="product?.name"
format="webp" format="webp"
densities="x1" densities="x1"
/> />
@ -22,28 +22,31 @@
</div> </div>
<nuxt-img <nuxt-img
:src="selectedImage" :src="selectedImage"
:alt="product.name" :alt="product?.name"
class="product__images-main" class="product__images-main"
format="webp" format="webp"
densities="x1" densities="x1"
/> />
</div> </div>
<div class="product__center"> <div class="product__center">
<p class="product__center-description">{{ product.description }}</p> <p class="product__center-description">{{ product?.description }}</p>
<client-only>
<p <p
v-if="attributes.length"
class="product__center-characteristic" class="product__center-characteristic"
@click="scrollTo('characteristics')" @click="scrollTo('characteristics')"
> >
{{ t('product.characteristics') }} {{ t('product.characteristics') }}
</p> </p>
</client-only>
</div> </div>
<div class="product__info"> <div class="product__info">
<div class="product__info-inner"> <div class="product__info-inner">
<div class="product__info-top"> <div class="product__info-top">
<p>{{ t('cards.product.stock') }} {{ product.quantity }}</p> <p>{{ t('cards.product.stock') }} {{ product?.quantity }}</p>
<nuxt-img <nuxt-img
:src="product.brand.smallLogo" :src="product?.brand.smallLogo"
:alt="product.brand.name" :alt="product?.brand.name"
format="webp" format="webp"
densities="x1" densities="x1"
/> />
@ -54,16 +57,18 @@
allow-half allow-half
disabled disabled
/> />
<div class="product__info-price">{{ product.price }} {{ CURRENCY }}</div> <div class="product__info-price">{{ product?.price }} {{ CURRENCY }}</div>
<client-only>
<div class="product__info-bottom"> <div class="product__info-bottom">
<ui-button <ui-button
class="product__info-button" class="product__info-button"
v-if="isProductInCart" v-if="isProductInCart"
@click="overwriteOrder({ @click="overwriteOrder({
type: 'remove', type: 'remove',
productUuid: product.uuid, productUuid: product?.uuid,
productName: product.name productName: product?.name
})" })"
:type="'button'"
:isLoading="removeLoading" :isLoading="removeLoading"
> >
{{ t('buttons.removeFromCart') }} {{ t('buttons.removeFromCart') }}
@ -73,9 +78,10 @@
v-else v-else
@click="overwriteOrder({ @click="overwriteOrder({
type: 'add', type: 'add',
productUuid: product.uuid, productUuid: product?.uuid,
productName: product.name productName: product?.name
})" })"
:type="'button'"
:isLoading="addLoading" :isLoading="addLoading"
> >
{{ t('buttons.addToCart') }} {{ t('buttons.addToCart') }}
@ -84,14 +90,15 @@
class="product__info-wishlist" class="product__info-wishlist"
@click="overwriteWishlist({ @click="overwriteWishlist({
type: (isProductInWishlist ? 'remove' : 'add'), type: (isProductInWishlist ? 'remove' : 'add'),
productUuid: product.uuid, productUuid: product?.uuid,
productName: product.name productName: product?.name
})" })"
> >
<icon name="mdi:cards-heart" size="28" v-if="isProductInWishlist" /> <icon name="mdi:cards-heart" size="28" v-if="isProductInWishlist" />
<icon name="mdi:cards-heart-outline" size="28" v-else /> <icon name="mdi:cards-heart-outline" size="28" v-else />
</div> </div>
</div> </div>
</client-only>
</div> </div>
</div> </div>
</div> </div>
@ -167,9 +174,11 @@ import {Navigation} from "swiper/modules";
import {CURRENCY} from "~/config/constants"; import {CURRENCY} from "~/config/constants";
import {useWishlistOverwrite} from "~/composables/wishlist"; import {useWishlistOverwrite} from "~/composables/wishlist";
import {useOrderOverwrite} from "~/composables/orders"; import {useOrderOverwrite} from "~/composables/orders";
import {useDefaultSeo} from "~/composables/seo";
import {useAppConfig} from "~/composables/config";
const route = useRoute(); const route = useRoute();
const {t} = useI18n(); const {t, locale} = useI18n();
const wishlistStore = useWishlistStore(); const wishlistStore = useWishlistStore();
const cartStore = useCartStore(); const cartStore = useCartStore();
@ -178,10 +187,44 @@ const { scrollTo } = useScrollTo();
const slug = useRouteParams<string>('slug'); const slug = useRouteParams<string>('slug');
const { APP_NAME } = useAppConfig();
const { overwriteWishlist } = useWishlistOverwrite(); const { overwriteWishlist } = useWishlistOverwrite();
const { addLoading, removeLoading, overwriteOrder } = useOrderOverwrite(); const { addLoading, removeLoading, overwriteOrder } = useOrderOverwrite();
const { product } = await useProductBySlug(slug.value); const { product, seoMeta } = await useProductBySlug(slug.value);
const { products, getProducts } = await useProducts();
const meta = useDefaultSeo(seoMeta.value || null);
if (meta) {
useSeoMeta({
title: meta.title || APP_NAME,
description: meta.description || meta.title || APP_NAME,
ogTitle: meta.og.title || undefined,
ogDescription: meta.og.description || meta.title || APP_NAME,
ogType: meta.og.type || undefined,
ogUrl: meta.og.url || undefined,
ogImage: meta.og.image || undefined,
twitterCard: meta.twitter.card || undefined,
twitterTitle: meta.twitter.title || undefined,
twitterDescription: meta.twitter.description || undefined,
robots: meta.robots,
});
useHead({
link: [
meta.canonical ? { rel: 'canonical', href: meta.canonical } : {},
].filter(Boolean) as any,
meta: [{ property: 'og:locale', content: locale.value }],
script: meta.jsonLd.map((obj: any) => ({
type: 'application/ld+json',
innerHTML: JSON.stringify(obj),
})),
__dangerouslyDisableSanitizersByTagID: Object.fromEntries(
meta.jsonLd.map((_, i: number) => [`ldjson-${i}`, ['innerHTML']])
),
});
}
const { products, getProducts } = useProducts();
await getProducts({ await getProducts({
categoriesSlugs: product.value?.category.slug categoriesSlugs: product.value?.category.slug
}); });

View file

@ -5,7 +5,12 @@
<p><span>{{ t('profile.cart.quantity') }}</span> {{ productsInCartQuantity }}</p> <p><span>{{ t('profile.cart.quantity') }}</span> {{ productsInCartQuantity }}</p>
<p><span>{{ t('profile.cart.total') }}: </span> {{ totalPrice }}{{ CURRENCY }}</p> <p><span>{{ t('profile.cart.total') }}: </span> {{ totalPrice }}{{ CURRENCY }}</p>
</div> </div>
<ui-button class="cart__top-button">{{ t('buttons.checkout') }}</ui-button> <ui-button
:type="'button'"
class="cart__top-button"
>
{{ t('buttons.checkout') }}
</ui-button>
</div> </div>
<div class="cart__list"> <div class="cart__list">
<div class="cart__list-inner" v-if="productsInCart.length"> <div class="cart__list-inner" v-if="productsInCart.length">

View file

@ -45,9 +45,9 @@ import {CURRENCY} from "~/config/constants";
import {useDate} from "~/composables/date"; import {useDate} from "~/composables/date";
const {t, locale} = useI18n(); const {t, locale} = useI18n();
const promocodesStore = usePromocodeStore(); const promocodeStore = usePromocodeStore();
const promocodes = computed(() => promocodesStore.promocodes); const promocodes = computed(() => promocodeStore.promocodes);
const copyCode = (code: string) => { const copyCode = (code: string) => {
navigator.clipboard.writeText(code) navigator.clipboard.writeText(code)

View file

@ -7,8 +7,6 @@ import { useAppConfig } from '~/composables/config';
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
const runtime = useRuntimeConfig(); const runtime = useRuntimeConfig();
const localeCookie = useCookie(useAppConfig().COOKIES_LOCALE_KEY);
const token = useCookie(useAppConfig().COOKIES_ACCESS_TOKEN_KEY);
const { $apollo } = nuxtApp as any; const { $apollo } = nuxtApp as any;
const errorLink = onError((err) => { const errorLink = onError((err) => {
@ -16,6 +14,9 @@ export default defineNuxtPlugin((nuxtApp) => {
}); });
const authLink = setContext(async (_, { headers }) => { const authLink = setContext(async (_, { headers }) => {
const localeCookie = useCookie(useAppConfig().COOKIES_LOCALE_KEY);
const token = useCookie(useAppConfig().COOKIES_ACCESS_TOKEN_KEY);
const hdrs: Record<string,string> = { const hdrs: Record<string,string> = {
...headers, ...headers,
'Accept-Language': localeCookie.value || 'en-gb' 'Accept-Language': localeCookie.value || 'en-gb'

View file

@ -1,2 +1,10 @@
User-Agent: * User-agent: *
Disallow: Disallow: /admin/
Disallow: /static/
Disallow: /media/
Disallow: /profile/
Allow: /
Sitemap: https://evibes.com/sitemap.xml
Host: evibes.com

View file

@ -1,4 +1,4 @@
import type {ICategory, ICategoryTag, IStoreFilters} from "~/types"; import type {ICategory, ICategoryTag, ISEOMeta, IStoreFilters} from "~/types";
export interface ICategoriesResponse { export interface ICategoriesResponse {
categories: { categories: {
@ -31,6 +31,16 @@ export interface ICategoryBySlugResponse {
} }
} }
export interface ICategoryBySlugSeoResponse {
categories: {
edges: {
node: {
seoMeta: ISEOMeta
}
}[]
}
}
export interface ICategoryTagsResponse { export interface ICategoryTagsResponse {
categoryTags: { categoryTags: {
edges: { edges: {

View file

@ -1,9 +1,11 @@
import type {ICategory} from "~/types"; import type {ICategory, ISEOMeta} from "~/types";
export interface IBrand { export interface IBrand {
name: string, name: string,
slug: string,
uuid: string, uuid: string,
smallLogo: string, smallLogo: string,
bigLogo: string, bigLogo: string,
seoMeta: ISEOMeta,
categories: ICategory[] categories: ICategory[]
} }

View file

@ -1,3 +1,5 @@
import type {ISEOMeta} from "~/types";
export interface IProduct { export interface IProduct {
uuid: string, uuid: string,
name: string, name: string,
@ -5,6 +7,7 @@ export interface IProduct {
quantity: number, quantity: number,
slug: string, slug: string,
description: string, description: string,
seoMeta: ISEOMeta,
brand: { brand: {
smallLogo: string, smallLogo: string,
uuid: string, uuid: string,

View file

@ -0,0 +1,20 @@
export interface ISEOMeta {
canonical: string,
description: string,
hreflang: string,
jsonLd: string,
openGraph: {
title: string,
description: string,
type: string,
url: string,
image: string
},
robots: string,
title: string,
twitter: {
card: string,
title: string,
description: string
}
}

View file

@ -11,6 +11,7 @@ export * from './app/orders'
export * from './app/category' export * from './app/category'
export * from './app/store' export * from './app/store'
export * from './app/promocodes' export * from './app/promocodes'
export * from './app/seometa'