This commit is contained in:
Parsa Nazer
2025-01-28 03:35:06 +03:30
16 changed files with 284 additions and 196 deletions
+3 -3
View File
@@ -23,10 +23,10 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`);
</script>
<template>
<div class="relative rounded-150 overflow-hidden w-full h-[500px]">
<div class="relative rounded-150 overflow-hidden w-full h-[500px] bg-white">
<img
:id="`category-image-${id}`"
class="absolute object-cover size-full"
class="absolute object-contain size-full"
:src="picture"
alt=""
/>
@@ -49,7 +49,7 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`);
<div class="typo-s-h-md">
{{ category }}
<span class="typo-p-xs -translate-y-1 inline-block mr-1">
24
{{ count }}
</span>
</div>
<span class="typo-p-md">محصولات ما را مشاهده کنید</span>
+37 -36
View File
@@ -3,6 +3,7 @@
import useGetAccount from "~/composables/api/account/useGetAccount";
import { useAuth } from "~/composables/api/auth/useAuth";
import useBaseUrl from "~/composables/global/useBaseUrl";
// types
@@ -15,6 +16,7 @@ type NavLink = {
const { data: account } = useGetAccount();
const { logout } = useAuth();
const baseUrl = useBaseUrl();
const nav_links = ref<NavLink[]>([
{
@@ -45,45 +47,44 @@ const nav_links = ref<NavLink[]>([
<div
class="size-full flex items-center justify-between container py-[2.25rem]"
>
<div
v-if="!!account"
class="w-2/12 flex items-center justify-start"
>
<span class="size-[2rem] bg-black rounded-full"></span>
<button @click="() => logout(true)">خروج از وبسایت</button>
</div>
<div class="flex items-center gap-16">
<div class="flex items-center justify-end gap-[1.5rem]">
<button
v-if="!!account"
:title="account.first_name + ' ' + account.last_name"
@click="() => logout(true)"
class="size-[1.5rem] relative overflow-hidden rounded-full bg-slate-300"
>
<img :src="baseUrl + account.profile_photo" alt="" />
</button>
<NuxtLink to="/signin" v-else class="flex-center">
<Icon name="ci:profile" size="20px" class="**:stroke-black" />
</NuxtLink>
<button class="flex-center">
<Icon name="ci:search" size="18px" class="**:stroke-black" />
</button>
<button class="flex-center">
<Icon name="ci:cart" size="20px" class="**:stroke-black" />
</button>
</div>
<button
@click="navigateTo('/signin')"
class="cursor-pointer"
v-else
>
وارد شوید
</button>
<nav
class="flex-center gap-[2.5rem] w-8/12 typo-label-sm text-slate-500"
>
<NuxtLink
v-for="(link, index) in nav_links"
:key="index"
:to="link.path"
<nav
class="flex-center gap-[2.5rem] typo-label-sm text-slate-600"
>
{{ link.title }}
</NuxtLink>
</nav>
<div class="w-2/12 flex items-center justify-end gap-[1.5rem]">
<button class="size-[1.5rem] flex-center">
<Icon name="ci:search" class="**:stroke-black" />
</button>
<button class="size-[1.5rem] flex-center">
<Icon name="ci:profile" class="**:stroke-black" />
</button>
<button class="size-[1.5rem] flex-center">
<Icon name="ci:cart" class="**:stroke-black" />
</button>
<NuxtLink
v-for="(link, index) in nav_links"
:key="index"
:to="link.path"
>
{{ link.title }}
</NuxtLink>
</nav>
</div>
<div>
LOGO
</div>
</div>
</header>
</template>
@@ -6,7 +6,7 @@ type Props = {
picture: string;
title: string;
color: string;
price: number;
price: string;
}
// props
@@ -99,14 +99,14 @@ watch(
<template>
<div class="size-full flex flex-col gap-14 justify-between">
<div class="w-full flex flex-col gap-10">
<div class="flex items-center justify-between w-full gap-5">
<div class="flex flex-col items-center w-full gap-5">
<div
class="flex items-center justify-start gap-2 text-lg w-full"
>
<Icon name="ci:filter-list" size="24" />
ترتیب بر اساس
</div>
<div class="w-full flex items-start justify-end gap-2">
<div class="w-full flex items-center gap-2">
<button
v-for="(sort, index) in sort_filter"
:key="index"
+53 -64
View File
@@ -4,6 +4,7 @@
import { Swiper, SwiperSlide } from "swiper/vue";
import type { SwiperClass } from "swiper/react";
import useHomeData from "~/composables/api/home/useHomeData";
type Props = {}
@@ -14,30 +15,10 @@ const {} = toRefs(props);
// state
const { data: homeData } = useHomeData();
const swiper_instance = ref<SwiperClass | null>(null);
const slides = [
{
id: 0,
title: "TEST"
},
{
id: 1,
title: "TEST"
},
{
id: 2,
title: "TEST"
},
{
id: 3,
title: "TEST"
},
{
id: 4,
title: "TEST"
}
];
// methods
@@ -48,50 +29,58 @@ const onSwiper = (swiper: SwiperClass) => {
</script>
<template>
<div class="w-full my-20 relative">
<Swiper
:slides-per-view="3.65"
:space-between="20"
:slides-offset-after="125"
:slides-offset-before="125"
@swiper="onSwiper"
>
<SwiperSlide
v-for="slide in slides"
:key="slide.id"
<section class="flex flex-col gap-4 bg-black h-[110svh] mt-40 py-32">
<div class="w-full flex justify-center items-center">
<span class="text-white typo-h-4">
دسته بندی ها
</span>
</div>
<div class="w-full my-20 relative">
<Swiper
:loop="true"
:centered-slides="true"
:slides-per-view="3.65"
:space-between="20"
@swiper="onSwiper"
>
<CategoryCard
:id="slide.id"
category="یک دسته بندی تست"
picture="/img/product-1.jpg"
:count="20"
description="یک دسته بندی تستasdasd"
<SwiperSlide
v-for="slide in homeData!.sub_categories"
:key="slide.id"
>
<CategoryCard
dark-layer
:id="slide.id"
:category="slide.name"
:picture="slide.icon"
:count="slide.product_count"
description="توضیحات دسته بندی"
/>
</SwiperSlide>
</Swiper>
<div
v-if="!swiper_instance?.isBeginning"
@click="swiper_instance?.slidePrev()"
class="absolute z-20 right-20 shadow-lg cursor-pointer shadow-black/25 bottom-[50%] bg-white rounded-full size-11.5 flex justify-center items-center"
>
<Icon
name="ci:arrow-right"
class="**:stroke-black"
size="24"
/>
</SwiperSlide>
</Swiper>
</div>
<div
v-if="!swiper_instance?.isBeginning"
@click="swiper_instance?.slidePrev()"
class="absolute z-20 right-20 shadow-lg cursor-pointer shadow-black/25 bottom-[50%] bg-white rounded-full size-11.5 flex justify-center items-center"
>
<Icon
name="ci:arrow-right"
class="**:stroke-black"
size="24"
/>
<div
v-if="!swiper_instance?.isEnd"
@click="swiper_instance?.slideNext()"
class="absolute z-20 left-20 shadow-lg cursor-pointer shadow-black/25 bottom-[50%] bg-white rounded-full size-11.5 flex justify-center items-center"
>
<Icon
name="ci:arrow-left"
class="**:stroke-black"
size="24"
/>
</div>
</div>
<div
v-if="!swiper_instance?.isEnd"
@click="swiper_instance?.slideNext()"
class="absolute z-20 left-20 shadow-lg cursor-pointer shadow-black/25 bottom-[50%] bg-white rounded-full size-11.5 flex justify-center items-center"
>
<Icon
name="ci:arrow-left"
class="**:stroke-black"
size="24"
/>
</div>
</div>
</section>
</template>
+24 -36
View File
@@ -4,34 +4,13 @@
import { Swiper, SwiperSlide } from "swiper/vue";
import type { SwiperClass } from "swiper/react";
import useHomeData from "~/composables/api/home/useHomeData";
// state
const { data: homeData } = useHomeData();
const swiper_instance = ref<SwiperClass | null>(null);
const slides = [
{
id: 0,
title: "TEST"
},
{
id: 1,
title: "TEST"
},
{
id: 2,
title: "TEST"
},
{
id: 3,
title: "TEST"
},
{
id: 4,
title: "TEST"
}
];
// methods
const onSwiper = (swiper: SwiperClass) => {
@@ -58,27 +37,36 @@ const onChange = (swiper: SwiperClass) => {
@slide-change="onChange"
>
<SwiperSlide
v-for="slide in slides"
v-for="slide in homeData!.sliders"
:key="slide.id"
>
<div class="relative w-full rounded-200 h-[80svh] overflow-hidden">
<img
<video
v-if="!!slide.video"
muted
autoplay
loop
class="absolute inset-0 size-full object-cover"
src="/img/hero-bg.jpg"
alt=""
:src="slide.video"
/>
<div class="size-full absolute z-10 bg-linear-to-t from-black to-transparent" />
<img
v-else
class="absolute inset-0 size-full object-cover"
:src="slide.image!"
:alt="slide.title"
/>
<div class="size-full absolute z-10 bg-linear-to-t from-black/50 to-transparent" />
<div class="px-20 absolute z-10 w-full bottom-36">
<div class="border-b border-white/10 pb-6">
<h3 class="typo-hero-1 text-white">
Samsung {{ slide.id }}
<div class="border-b border-white/10 pb-6 flex flex-col gap-4">
<h3 class="typo-h-1 tracking-[-2px] text-white">
{{ slide.title }}
</h3>
<div class="flex justify-between items-end">
<span class="typo-p-lg text-white">
توضیحات درمورد این محصول خاص
</span>
<span class="typo-p-lg text-white">
{{ slide.description }}
</span>
<Button class="invert rounded-full hover:bg-transparent">
خرید Samsung
مشاهده
</Button>
</div>
</div>
@@ -98,7 +86,7 @@ const onChange = (swiper: SwiperClass) => {
</button>
<div class="flex items-center justify-center gap-3 text-white">
<div
v-for="(slide, index) in slides"
v-for="(_slide, index) in homeData!.sliders"
:class="swiper_instance?.realIndex === index ? 'bg-white' : 'bg-transparent'"
class="border border-white size-3 rounded-full transition-all duration-200"
@click="swiper_instance?.slideTo(index)"
+35 -20
View File
@@ -1,6 +1,13 @@
<script setup lang="ts">
// import
import useHomeData from "~/composables/api/home/useHomeData";
// state
const { data: homeData } = useHomeData();
const clipPathPercent = ref(49);
const draggableEl = ref<HTMLElement | null>(null);
@@ -29,56 +36,64 @@ watch(
<template>
<div class="container">
<div class="flex flex-col items-center gap-3 mb-16">
<span class="typo-p-md text-slate-500">یک متن تست لورم</span>
<span class="typo-h-3 text-black"
>تفاوت محصول را ببینید در اینجا</span
>
<span class="typo-p-md text-slate-500">مقایسه محصولات</span>
<span class="typo-h-3 text-black">
تفاوت محصلات ما را ببینید
</span>
</div>
<div
ref="previewContainerEl"
class="rounded-200 overflow-hidden h-[90svh] relative"
>
<img
src="/img/hero-bg.jpg"
class="select-none absolute size-full object-cover"
alt=""
:src="homeData!.difreance_section.image1"
class="select-none absolute size-full object-cover brightness-[95%]"
:alt="homeData!.difreance_section.title1"
/>
<div class="absolute size-full right-0 w-full">
<img
src="/img/hero-bg.jpg"
class="overlay-image select-none absolute object-cover size-full hue-rotate-200 brightness-35"
alt=""
:src="homeData!.difreance_section.image2"
class="overlay-image select-none absolute object-cover size-full brightness-[95%]"
:alt="homeData!.difreance_section.title2"
/>
<div
:style="{
left: `${clipPathPercent}%`,
}"
ref="draggableEl"
class="select-none w-2 h-full bg-white absolute left-0 flex items-center justify-center"
class="select-none w-2 h-full bg-black absolute left-0 flex items-center justify-center"
>
<div
class="cursor-grab hover:scale-115 transition-transform rounded-full absolute bg-white size-11 flex items-center justify-center"
class="cursor-grab hover:scale-115 transition-transform rounded-full absolute bg-black size-11 flex items-center justify-center"
>
<Icon
name="ci:arrows"
size="24"
class="**:stroke-black"
class="**:stroke-white"
/>
</div>
</div>
</div>
<div
class="absolute bottom-0 p-10 w-full flex justify-between items-end bg-linear-to-t from-black/55 to-transparent"
class="absolute bottom-0 p-10 w-full flex justify-between items-end"
>
<div class="flex flex-col gap-2 text-white">
<span class="typo-p-md">رنگ محصول</span>
<span class="typo-h-3">نارنجی</span>
<div class="flex flex-col gap-2 text-black">
<span class="typo-p-md">
{{ homeData!.difreance_section.description1 }}
</span>
<span class="typo-h-3">
{{ homeData!.difreance_section.title1 }}
</span>
</div>
<div class="flex flex-col justify-start gap-2 text-white">
<span class="typo-p-md">رنگ محصول</span>
<span class="typo-h-3">سفید</span>
<div class="flex flex-col gap-2 text-black">
<span class="typo-p-md text-end">
{{ homeData!.difreance_section.description2 }}
</span>
<span class="typo-h-3 text-end">
{{ homeData!.difreance_section.title2 }}
</span>
</div>
</div>
</div>
@@ -1,14 +1,5 @@
<script setup lang="ts">
// types
type Props = {}
// props
const props = defineProps<Props>();
const {} = toRefs(props);
// state
const isOpen = ref(false);
+29 -9
View File
@@ -1,15 +1,35 @@
<script lang="ts" setup></script>
<script lang="ts" setup>
// import
import useGetProduct from "~/composables/api/product/useGetProduct";
// state
const route = useRoute();
const id = route.params.id as string | undefined;
const { data: product } = useGetProduct(id);
</script>
<template>
<section class="h-[95svh] w-full relative bg-black mt-[5rem]">
<img src="/img/product-3.jpg" class="object-cover absolute size-full" />
<div class="size-full absolute inset-0 bg-black/60" />
<section class="h-[110svh] w-full relative bg-black mt-[5rem]">
<video
src="/video/product-video.mp4"
class="object-cover absolute size-full"
muted
autoplay
loop
/>
<div class="size-full absolute inset-0 bg-black/20" />
<StickyCard
color="سبز"
:price="240000"
picture="/img/product-1.jpg"
title="نام محصول"
class="absolute right-6 bottom-6"
color="آبی"
:price="product!.price"
picture="/img/product-6.webp"
:title="product!.name"
class="absolute right-10 bottom-10"
/>
</section>
</template>
@@ -0,0 +1,55 @@
// imports
import { useQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
// types
export type GetHomeDataResponse = {
"sliders": {
"id": number,
"link": string,
"title": string,
"description": string,
"image": string | null,
"video": string | null
}[],
"sub_categories": {
"id": number,
"name": string,
"icon": string,
"product_count": number,
}[],
"products": Product[],
"difreance_section": {
"image1": string,
"image2": string,
"title1": string,
"title2": string,
"description1": string,
"description2": string,
"link1": string,
"link2": string
}
};
const useHomeData = () => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleHomeData = async () => {
const { data } = await axios.get<GetHomeDataResponse>(`${API_ENDPOINTS.home}`);
return data;
};
return useQuery({
queryKey: [QUERY_KEYS.home],
queryFn: () => handleHomeData()
});
};
export default useHomeData;
+11 -9
View File
@@ -1,40 +1,42 @@
export const API_ENDPOINTS = {
home : "/home",
account: {
profile: "/accounts/profile",
send_otp: "/accounts/send_otp",
send_otp: "/accounts/send_otp"
},
product: {
get: "/products",
get: "/products"
},
auth: {
refresh: "/token/refresh",
verify: "/accounts/verify",
signin: "/token",
logout: "/accounts/logout",
logout: "/accounts/logout"
},
chat: {
messages: "/chat/product",
new_message: "/chat/product",
new_message: "/chat/product"
},
products: {
get_all: "/products",
categories: "/products/categories",
},
categories: "/products/categories"
}
};
export const QUERY_KEYS = {
home: "home",
chat: "chat",
product: "product",
products: "products",
account: "account",
categories: "categories",
categories: "categories"
};
export const MUTATION_KEYS = {
create_chat: "create_chat",
create_chat: "create_chat"
};
export const PRODUCT_RANGE = {
min: 0,
max: 100_000_000,
max: 100_000_000
};
+3
View File
@@ -1,4 +1,7 @@
<script lang="ts" setup>
// import
import useGetCategories from "~/composables/api/product/useGetCategories";
// state
+22 -4
View File
@@ -1,6 +1,25 @@
<script setup lang="ts">
<script lang="ts" setup>
import Categories from "~/components/home/Categories.vue";
// import
import useHomeData from "~/composables/api/home/useHomeData";
// state
const { suspense } = useHomeData();
// lifecycle
onServerPrefetch(async () => {
const response = await suspense();
if (response.isError) {
throw createError({
statusCode: 500,
statusMessage: `Landing error : ${response.error.message}`,
})
}
});
</script>
@@ -9,9 +28,8 @@ import Categories from "~/components/home/Categories.vue";
<Hero />
<Preview />
<Categories />
<ProductsSlider title="یک عنوان تستی" />
<ProductsSlider title="محصولات پرفروش" />
<Brands />
<!-- <ProductHero />-->
<MostRecentComments />
<LatestStories />
</div>
+4 -1
View File
@@ -1,6 +1,7 @@
import axiosOriginal from "axios";
import { useAuth } from "~/composables/api/auth/useAuth";
import { API_ENDPOINTS } from "~/constants";
import Logger from "~/tools/logger";
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
@@ -23,7 +24,9 @@ export default defineNuxtPlugin(() => {
axios.interceptors.response.use((response) => {
return response;
}, function(error) {
}, async function(error) {
await Logger.axiosErrorLog(error);
// if (error.status === 401) {
// logout();
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

+5 -2
View File
@@ -24,11 +24,14 @@ declare global {
type Product = {
id: number;
price: number;
price: string;
name: string;
description: string;
currency: string;
image: string;
"video": string | null,
"image1": string,
"image2": string,
"image3": string,
rating: number;
view: number;
sell: number;