Updated
This commit is contained in:
@@ -18,16 +18,16 @@ const { logout } = useAuth();
|
|||||||
|
|
||||||
const nav_links = ref<NavLink[]>([
|
const nav_links = ref<NavLink[]>([
|
||||||
{
|
{
|
||||||
title: "فروشگاه",
|
title: "خانه",
|
||||||
path: "#"
|
path: "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "محصولات",
|
||||||
|
path: "/products"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "دسته بندی ها",
|
title: "دسته بندی ها",
|
||||||
path: "#"
|
path: "/category"
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "جستجو",
|
|
||||||
path: "#"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "ارتباط با ما",
|
title: "ارتباط با ما",
|
||||||
@@ -62,9 +62,9 @@ const nav_links = ref<NavLink[]>([
|
|||||||
<NuxtLink to="/signin" v-else class="flex-center">
|
<NuxtLink to="/signin" v-else class="flex-center">
|
||||||
<Icon name="ci:profile" size="24px" class="**:stroke-black" />
|
<Icon name="ci:profile" size="24px" class="**:stroke-black" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<button class="flex-center">
|
<NuxtLink to="/products" class="flex-center">
|
||||||
<Icon name="ci:search" size="21px" class="**:stroke-black" />
|
<Icon name="ci:search" size="21px" class="**:stroke-black" />
|
||||||
</button>
|
</NuxtLink>
|
||||||
<button class="flex-center">
|
<button class="flex-center">
|
||||||
<Icon name="ci:cart" size="24px" class="**:stroke-black" />
|
<Icon name="ci:cart" size="24px" class="**:stroke-black" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||||
import type { SwiperClass } from "swiper/react";
|
import type { SwiperClass } from "swiper/react";
|
||||||
|
import useHomeData from "~/composables/api/home/useHomeData";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ defineProps<Props>();
|
|||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
|
const { data : homeData } = useHomeData();
|
||||||
|
|
||||||
const swiper_instance = ref<SwiperClass | null>(null);
|
const swiper_instance = ref<SwiperClass | null>(null);
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
@@ -76,15 +79,16 @@ const onSwiper = (swiper: SwiperClass) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<Swiper :slides-per-view="3" :space-between="24" @swiper="onSwiper">
|
<Swiper :slides-per-view="3" :space-between="24" @swiper="onSwiper">
|
||||||
<SwiperSlide v-for="i in 4" :key="i">
|
<SwiperSlide v-for="product in homeData!.products" :key="product.id">
|
||||||
<ProductCard
|
<ProductCard
|
||||||
brand="Samsung"
|
:id="product.id"
|
||||||
title="Galaxy S20 Ultra"
|
brand="برند محصول"
|
||||||
picture="/assets/img/product-1.jpg"
|
:title="product.name"
|
||||||
:colors="['#0000ff', '#00ff00', 'red']"
|
:picture="product.image1"
|
||||||
:price="599"
|
:colors="['white', 'black']"
|
||||||
:rate="2.4"
|
:price="product.price"
|
||||||
tag="New"
|
:rate="product.rating"
|
||||||
|
:dark-layer="true"
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
|||||||
@@ -35,20 +35,20 @@ const changeSlide = (id: number) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col relative gap-4">
|
<div class="flex flex-col relative gap-6">
|
||||||
<div class="bg-red-300 w-full relative aspect-square overflow-hidden rounded-200">
|
<div class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-200">
|
||||||
<img
|
<img
|
||||||
class="size-full absolute object-cover"
|
class="size-full absolute object-contain"
|
||||||
:src="selectedSlideDetail.picture"
|
:src="selectedSlideDetail.picture"
|
||||||
:alt="String(selectedSlideDetail.id)"
|
:alt="String(selectedSlideDetail.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-6">
|
||||||
<div
|
<div
|
||||||
@click="changeSlide(slide.id)"
|
@click="changeSlide(slide.id)"
|
||||||
v-for="slide in slides"
|
v-for="slide in slides"
|
||||||
:class="selectedSlide === slide.id ? 'ring-black' : 'ring-transparent'"
|
:class="selectedSlide === slide.id ? 'ring-black' : 'ring-transparent'"
|
||||||
class="cursor-pointer aspect-square w-[108px] ring-2 ring-offset-4 rounded-200 w-full overflow-hidden relative"
|
class="cursor-pointer brightness-[97%] bg-white aspect-square ring-2 ring-offset-4 rounded-200 w-full overflow-hidden relative"
|
||||||
:key="slide.id"
|
:key="slide.id"
|
||||||
>
|
>
|
||||||
<img class="absolute object-cover size-full" :src="slide.picture" :alt="String(slide.id)" />
|
<img class="absolute object-cover size-full" :src="slide.picture" :alt="String(slide.id)" />
|
||||||
|
|||||||
@@ -4,62 +4,88 @@
|
|||||||
import Tag from "~/components/global/Tag.vue";
|
import Tag from "~/components/global/Tag.vue";
|
||||||
import Rate from "~/components/global/Rate.vue";
|
import Rate from "~/components/global/Rate.vue";
|
||||||
import ColorCircle from "~/components/global/ColorCircle.vue";
|
import ColorCircle from "~/components/global/ColorCircle.vue";
|
||||||
|
import { useImageColor } from "~/composables/global/useImageColor";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
id: number,
|
||||||
brand: string;
|
brand: string;
|
||||||
title: string;
|
title: string;
|
||||||
colors: string[];
|
colors: string[];
|
||||||
price: number;
|
price: string;
|
||||||
picture: string;
|
picture: string;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
rate?: number;
|
rate?: number;
|
||||||
|
darkLayer?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// props
|
// props
|
||||||
|
|
||||||
defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
const { id } = toRefs(props);
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { colorObject } = useImageColor(`#product-image-${id.value}`);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<NuxtLink :to="'/product/' + id">
|
||||||
class="relative size-full min-h-[31.25rem] rounded-2xl bg-black/10 overflow-hidden p-6"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="~/assets/img/product-2.jpg"
|
|
||||||
class="size-full object-cover absolute inset-0"
|
|
||||||
alt="product-background"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
class="flex justify-between items-center absolute px-6 pt-6 top-0 w-full inset-x-0"
|
class="relative size-full min-h-[31.25rem] rounded-2xl bg-white brightness-[98%] overflow-hidden p-6"
|
||||||
>
|
>
|
||||||
<Rate v-if="rate">
|
<img
|
||||||
{{ rate }}
|
:id="`product-image-${id}`"
|
||||||
</Rate>
|
:src="picture"
|
||||||
<Tag v-if="tag">
|
class="size-full object-contain absolute inset-0"
|
||||||
{{ tag }}
|
alt="product-background"
|
||||||
</Tag>
|
/>
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="absolute inset-x-0 bottom-0 pb-6 px-6 flex flex-row-reverse justify-between items-end"
|
v-if="darkLayer"
|
||||||
>
|
class="bg-linear-to-t inset-0 from-black/50 to-transparent to-40% absolute z-10 size-full"
|
||||||
<span class="typo-p-md"> {{ price }} </span>
|
/>
|
||||||
<div class="flex flex-col gap-2 items-start">
|
|
||||||
<span class="typo-p-md">
|
<div
|
||||||
|
class="flex justify-between items-center absolute px-6 pt-6 top-0 w-full inset-x-0"
|
||||||
|
>
|
||||||
|
<Rate v-if="rate" :rate="rate"/>
|
||||||
|
<Tag v-if="tag">
|
||||||
|
{{ tag }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
colorObject?.isLight && !darkLayer
|
||||||
|
? 'text-black'
|
||||||
|
: 'text-white'
|
||||||
|
"
|
||||||
|
class="absolute inset-x-0 bottom-0 pb-6 px-6 flex flex-row-reverse justify-between items-end z-10"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 items-start w-full">
|
||||||
|
<span class="typo-p-md font-medium">
|
||||||
{{ brand }}
|
{{ brand }}
|
||||||
</span>
|
</span>
|
||||||
<span class="typo-sub-h-md">
|
<span class="typo-sub-h-lg">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</span>
|
</span>
|
||||||
<!-- <div class="flex items-center gap-2 mt-1">
|
<div class="flex items-center justify-between w-full mt-1">
|
||||||
<ColorCircle
|
<div class="flex items-center gap-2 mt-1">
|
||||||
v-for="color in colors"
|
<ColorCircle
|
||||||
:key="color"
|
v-for="color in colors"
|
||||||
:style="{ backgroundColor: color }"
|
:key="color"
|
||||||
/>
|
:style="{ backgroundColor: color }"
|
||||||
</div> -->
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="typo-p-md font-medium whitespace-nowrap">
|
||||||
|
{{ price }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -131,16 +131,6 @@ watch(
|
|||||||
دسته بندی
|
دسته بندی
|
||||||
</div>
|
</div>
|
||||||
<ComboBox :options="options" v-model="params.category" />
|
<ComboBox :options="options" v-model="params.category" />
|
||||||
<div
|
|
||||||
v-if="params.category"
|
|
||||||
class="w-full flex flex-wrap gap-2 px-[1rem]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="py-1 px-3 cursor-pointer text-nowrap bg-slate-100 rounded-full text-sm"
|
|
||||||
>
|
|
||||||
{{ params.category }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full gap-5">
|
<div class="flex flex-col w-full gap-5">
|
||||||
@@ -177,7 +167,7 @@ watch(
|
|||||||
<span class="text-sm text-black">
|
<span class="text-sm text-black">
|
||||||
{{
|
{{
|
||||||
"price_gte" in params
|
"price_gte" in params
|
||||||
? params.price_gte.toLocaleString()
|
? sliderValue[0].toLocaleString()
|
||||||
: PRODUCT_RANGE.min
|
: PRODUCT_RANGE.min
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
@@ -187,7 +177,7 @@ watch(
|
|||||||
<span class="text-sm text-black">
|
<span class="text-sm text-black">
|
||||||
{{
|
{{
|
||||||
"price_lte" in params
|
"price_lte" in params
|
||||||
? params.price_lte.toLocaleString()
|
? sliderValue[1].toLocaleString()
|
||||||
: PRODUCT_RANGE.max
|
: PRODUCT_RANGE.max
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
@@ -240,5 +230,3 @@ watch(
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="bg-slate-50 p-20">
|
<section class="bg-slate-50">
|
||||||
<div class="flex gap-12 my-42 container">
|
<div class="flex gap-12 my-42 container">
|
||||||
<div class="flex flex-col gap-6 min-w-fit">
|
<div class="flex flex-col gap-6 min-w-fit">
|
||||||
<h3 class="typo-h-3">
|
<h3 class="typo-h-3">
|
||||||
|
|||||||
@@ -14,20 +14,22 @@ const { data: product } = useGetProduct(id);
|
|||||||
const quantity = ref(1);
|
const quantity = ref(1);
|
||||||
|
|
||||||
const selectedSlide = ref(0);
|
const selectedSlide = ref(0);
|
||||||
const slides = [
|
const slides = computed(() => {
|
||||||
{
|
return [
|
||||||
id: 0,
|
{
|
||||||
picture: "/img/product-1.jpg",
|
id: 0,
|
||||||
},
|
picture: product.value!.image1
|
||||||
{
|
},
|
||||||
id: 1,
|
{
|
||||||
picture: "/img/product-2.jpg",
|
id: 1,
|
||||||
},
|
picture: product.value!.image2
|
||||||
{
|
},
|
||||||
id: 2,
|
{
|
||||||
picture: "/img/product-3.jpg",
|
id: 2,
|
||||||
},
|
picture: product.value!.image3
|
||||||
];
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -45,7 +47,7 @@ const slides = [
|
|||||||
<Rating />
|
<Rating />
|
||||||
</div>
|
</div>
|
||||||
<p class="typo-p-md text-slate-500 text-justify">
|
<p class="typo-p-md text-slate-500 text-justify">
|
||||||
{{product!.description}}
|
{{ product!.description }}
|
||||||
</p>
|
</p>
|
||||||
<div class="w-full flex flex-col gap-6 mt-4">
|
<div class="w-full flex flex-col gap-6 mt-4">
|
||||||
<RemainQuantity
|
<RemainQuantity
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ const { data: product } = useGetProduct(id);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="h-[110svh] w-full relative bg-black mt-[5rem]">
|
<section v-if="product?.video" class="h-[110svh] w-full relative bg-black mt-[5rem]">
|
||||||
<video
|
<video
|
||||||
src="/video/product-video.mp4"
|
:src="product.video"
|
||||||
class="object-cover absolute size-full"
|
class="object-cover absolute size-full"
|
||||||
muted
|
muted
|
||||||
autoplay
|
autoplay
|
||||||
|
|||||||
+37
-21
@@ -14,13 +14,18 @@ const debouncedSearch = refDebounced(search, 300);
|
|||||||
// computed
|
// computed
|
||||||
|
|
||||||
const filteredCategories = computed(() => {
|
const filteredCategories = computed(() => {
|
||||||
|
|
||||||
if (debouncedSearch.value.length > 0) {
|
if (debouncedSearch.value.length > 0) {
|
||||||
return categories.value?.filter((cat) =>
|
return categories.value!.map((cat) => {
|
||||||
cat.name.includes(debouncedSearch.value)
|
cat.subcategorys = cat.subcategorys.filter((subcat) => {
|
||||||
);
|
return subcat.name.includes(debouncedSearch.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return cat;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return categories.value;
|
return categories.value!;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ssr
|
// ssr
|
||||||
@@ -32,10 +37,10 @@ await useAsyncData(async () => {
|
|||||||
if (response.isError) {
|
if (response.isError) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
statusMessage: `Error in categories page prefetch`,
|
statusMessage: `Error in categories page prefetch`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -61,21 +66,32 @@ await useAsyncData(async () => {
|
|||||||
</Input>
|
</Input>
|
||||||
</div>
|
</div>
|
||||||
<Transition name="fade" mode="out-in">
|
<Transition name="fade" mode="out-in">
|
||||||
<div
|
<div v-if="filteredCategories">
|
||||||
v-if="filteredCategories?.length !== 0"
|
<div
|
||||||
v-auto-animate
|
class="flex flex-col gap-6"
|
||||||
class="grid grid-cols-3 gap-4 w-full mt-12"
|
v-for="mainCategory in filteredCategories"
|
||||||
>
|
>
|
||||||
<CategoryCard
|
<div class="w-full flex items-center justify-between">
|
||||||
v-for="category in filteredCategories"
|
<span>
|
||||||
:key="category.id"
|
{{ mainCategory.name }}
|
||||||
:id="category.id"
|
</span>
|
||||||
:category="category.name"
|
</div>
|
||||||
:picture="category.icon"
|
<div
|
||||||
:count="20"
|
v-auto-animate
|
||||||
description="یک دسته بندی تستasdasd"
|
class="grid grid-cols-3 gap-4 w-full mt-12"
|
||||||
dark-layer
|
>
|
||||||
/>
|
<CategoryCard
|
||||||
|
v-for="category in mainCategory.subcategorys"
|
||||||
|
:key="category.id"
|
||||||
|
:id="category.id"
|
||||||
|
:category="category.name"
|
||||||
|
:picture="category.icon"
|
||||||
|
:count="20"
|
||||||
|
description="یک دسته بندی تستasdasd"
|
||||||
|
dark-layer
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="flex w-full mt-12">
|
<div v-else class="flex w-full mt-12">
|
||||||
|
|||||||
Vendored
+1
-5
@@ -48,9 +48,6 @@ declare global {
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
meta_title: string;
|
|
||||||
meta_description: string;
|
|
||||||
parent: number;
|
|
||||||
"product_count": string,
|
"product_count": string,
|
||||||
"subcategorys": SubCategory[]
|
"subcategorys": SubCategory[]
|
||||||
};
|
};
|
||||||
@@ -60,9 +57,8 @@ declare global {
|
|||||||
"name": string,
|
"name": string,
|
||||||
"slug": string,
|
"slug": string,
|
||||||
"icon": string,
|
"icon": string,
|
||||||
"meta_title": string,
|
|
||||||
"meta_description": string,
|
|
||||||
"product_count": string,
|
"product_count": string,
|
||||||
|
"parent": string,
|
||||||
"show": boolean
|
"show": boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user