This commit is contained in:
marzban-dev
2025-01-28 22:09:53 +03:30
parent e12173a372
commit 85e4296e60
10 changed files with 145 additions and 113 deletions
+9 -9
View File
@@ -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">
+17 -15
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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">
+1 -5
View File
@@ -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
} }