This commit is contained in:
Parsa Nazer
2025-01-26 03:09:38 +03:30
16 changed files with 623 additions and 52 deletions
+54 -2
View File
@@ -1,7 +1,59 @@
<script setup lang="ts"></script>
<script setup lang="ts">
// types
type Props = {
address?: Address;
isSelected?: boolean;
};
// props
const props = defineProps<Props>();
// emit
const emit = defineEmits(["select"]);
// computed
</script>
<template>
<div></div>
<button
@click="!!address ? emit('select', address) : null"
:class="
isSelected
? 'border-cyan-500 ring-2 ring-offset-2 ring-cyan-500'
: 'border-gray-300'
"
class="flex flex-col items-center transition-all cursor-pointer w-full gap-4 p-4 border rounded-xl bg-gray-50"
>
<span
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900"
>
آدرس
</span>
<div
class="flex flex-col items-center justify-between w-full gap-4 lg:flex-row"
>
<span
class="w-full text-start text-sm lg:text-[1rem] lg:w-9/12 text-gray-700"
>
{{
!!address
? `ایران, ${address.province}, ${address.city}, ${address.full_address}, کدپستی ${address.postal_code}`
: "افزودن آدرس جدید"
}}
</span>
<div class="flex items-center justify-end w-full lg:w-3/12">
<AddressModal
@add="(data) => $emit('add', data)"
:address="address"
/>
</div>
</div>
</button>
</template>
<style scoped></style>
+211
View File
@@ -0,0 +1,211 @@
<script setup lang="ts">
// types
type Props = {
address?: Address;
};
// props
const props = defineProps<Props>();
const { address } = toRefs(props);
// emit
const emit = defineEmits(["add"]);
// state
const isShow = ref(false);
const addressData = ref<
Pick<Address, "province" | "city" | "postal_code" | "full_address">
>({
province: address.value?.province ?? "",
city: address.value?.city ?? "",
postal_code: address.value?.postal_code ?? "",
full_address: address.value?.full_address ?? "",
});
// computed
const isEditing = computed(() => !!address.value);
// methods
const closeModal = () => {
if (!isEditing.value) {
addressData.value = {
province: "",
city: "",
postal_code: "",
full_address: "",
};
}
isShow.value = false;
};
const addNew = () => {
emit("add", { ...addressData.value, id: Date.now() });
closeModal();
};
</script>
<template>
<DialogRoot
v-model:open="isShow"
@update:open="
(state) => {
!state ? closeModal() : null;
}
"
>
<DialogTrigger>
<button
class="flex items-center gap-1 rtl:flex-row-reverse font-iran-yekan-x cursor-pointer"
>
<span class="font-bold text-cyan-500 text-sm lg:text-[1rem]">
{{ !!address ? "ویرایش آدرس" : "افزودن آدرس" }}
</span>
<Icon
v-if="!!address"
name="bi:pen"
class="**:fill-cyan-500"
size="16"
/>
<Icon
v-else
name="bi:plus"
class="**:stroke-cyan-500"
size="20"
/>
</button>
</DialogTrigger>
<DialogPortal>
<DialogOverlay
class="bg-black/20 data-[state=open]:animate-overlay-show fixed inset-0 z-30"
/>
<DialogContent
class="data-[state=open]:animate-content-show text-black font-iran-yekan-x fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[50rem] translate-x-[-50%] translate-y-[-50%] rounded-3xl bg-white shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none z-[100]"
>
<div
class="w-full flex items-center px-6 justify-between py-[1.8rem] border-b border-slate-200"
>
<DialogClose
class="inline-flex size-8 items-center justify-center transition-all rounded-full bg-gray-50 border border-slate-200 hover:border-black focus:outline-none"
aria-label="Close"
>
<Icon name="bi:x-lg" class="**:fill-red-600" />
</DialogClose>
<DialogTitle class="typo-sub-h-xl font-semibold">
{{ !!address ? "ویرایش آدرس" : "افزودن آدرس" }}
</DialogTitle>
</div>
<div class="flex-col-center gap-6 px-6 py-10" dir="rtl">
<div
class="grid w-full grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"
>
<div class="flex flex-col gap-2">
<label
for="province"
class="text-xs font-semibold lg:text-sm text-gray-900"
>استان
<span class="text-sm text-red-500"
>*</span
></label
>
<ComboBox
id="province"
:options="[
{
name: 'استان ها',
children: [
{ name: 'مشهد' },
{ name: 'مشهد' },
{ name: 'مشهد' },
],
},
]"
v-model="addressData.province"
/>
</div>
<div class="flex flex-col gap-2">
<label
for="city"
class="text-xs font-semibold lg:text-sm text-gray-900"
>شهر
<span class="text-sm text-red-500"
>*</span
></label
>
<ComboBox
id="city"
:options="[
{
name: 'استان ها',
children: [
{ name: 'مشهد' },
{ name: 'مشهد' },
{ name: 'مشهد' },
],
},
]"
v-model="addressData.city"
/>
</div>
<div class="flex flex-col gap-2">
<label
for="post"
class="text-xs font-semibold lg:text-sm text-gray-900"
>کد پستی
<span class="text-sm text-red-500"
>*</span
></label
>
<Input
id="post"
type="text"
placeholder="جست و جو"
class="rounded-xl"
v-model="addressData.postal_code"
/>
</div>
</div>
<div class="flex flex-col w-full gap-2">
<label
for="address"
class="text-xs font-semibold lg:text-sm text-gray-900"
>آدرس کامل
<span class="text-sm text-red-500">*</span></label
>
<textarea
id="address"
placeholder="آدرس خود را بنویسید"
v-model="addressData.full_address"
class="flex items-center field-sizing-content resize-none bg-slate-50 border-slate-200 hover:border-black max-h-[10rem] text-black justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3.5 selection:bg-slate-100 rounded-100 outline-none flex-1 text-sm placeholder-slate-400"
></textarea>
</div>
</div>
<div class="p-6 border-t border-slate-200 flex gap-3">
<Button @click="addNew" class="rounded-full px-10"
>ثبت</Button
>
<DialogClose aria-label="Close">
<Button variant="outlined" class="rounded-full px-10">
انصراف
</Button>
</DialogClose>
</div>
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>
<style scoped></style>
+5 -5
View File
@@ -17,7 +17,7 @@ const handleDeleteFromCart = () => {};
class="size-[88px] aspect-square shrink-0 rounded-100 border border-gray-300 overflow-hidden"
>
<img
src="../assets/images/products/product-1.png"
src="/img/product-1.jpg"
class="object-cover size-full"
alt="product"
/>
@@ -47,13 +47,13 @@ const handleDeleteFromCart = () => {};
>
<Icon
v-if="counter == 1"
name="bi:dash"
class="**:stroke-gray-800"
name="bi:trash"
class="**:fill-red-700"
/>
<Icon
v-else
name="bi:trash"
class="**:fill-red-700"
name="bi:dash"
class="**:stroke-gray-800"
/>
</button>
</div>
+12 -1
View File
@@ -1,7 +1,18 @@
<script setup lang="ts"></script>
<template>
<div></div>
<div class="flex items-center w-full gap-4">
<div
class="size-[3.5rem] shrink-0 rounded-100 border border-gray-300 overflow-hidden"
>
<img src="/img/product-1.jpg" alt="product" class="object-conver" />
</div>
<span
class="text-xs font-semibold lg:text-sm text-gray-800 line-clamp-2"
>
فشارسنج بازویی امرن Omron M3
</span>
</div>
</template>
<style scoped></style>
+8 -5
View File
@@ -9,11 +9,14 @@ type Option = {
type Props = {
options: Option[];
modelValue: string[];
placeholder?: string;
};
// props
const props = defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
placeholder: "جست و جو",
});
const { modelValue } = toRefs(props);
@@ -48,11 +51,11 @@ watch(
<template>
<ComboboxRoot class="relative" dir="rtl" v-model="value">
<ComboboxAnchor
class="w-full inline-flex items-center justify-between rounded-full border border-slate-200 hover:border-slate-300 focus:border-slate-800 px-[1rem] text-sm leading-none py-3.5 gap-[5px] bg-slate-50 text-black hover:bg-white/90 transition-all data-[placeholder]:text-black/80 typo-label-sm outline-none"
class="w-full inline-flex items-center justify-between rounded-xl border-[1.5px] border-slate-200 focus:border-slate-800 px-[1rem] text-sm leading-none py-3.5 gap-[5px] bg-slate-50 text-black hover:border-black transition-all data-[placeholder]:text-black/80 typo-label-sm outline-none"
>
<ComboboxInput
class="!bg-transparent outline-none text-black h-full selection:bg-slate-100 placeholder-stone-400"
placeholder="جست و جوی دسته بندی"
class="!bg-transparent outline-none text-black h-full selection:bg-slate-100 placeholder-slate-400"
:placeholder="placeholder"
/>
<ComboboxTrigger>
<Icon name="ci:chevron-down" class="size-5" />
@@ -60,7 +63,7 @@ watch(
</ComboboxAnchor>
<ComboboxContent
class="absolute z-10 w-full mt-1.5 bg-slate-50 overflow-hidden rounded-3xl shadow-sm border border-slate-200 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=bottom]:animate-slideUpAndFade"
class="absolute z-10 w-full mt-1.5 bg-slate-50 overflow-hidden rounded-xl shadow-sm border border-slate-200 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=bottom]:animate-slideUpAndFade"
>
<ComboboxViewport class="p-[1rem]">
<ComboboxEmpty
+5 -7
View File
@@ -1,5 +1,4 @@
<script setup lang="ts">
type Props = {
variant?: "solid" | "outlined";
disabled?: boolean;
@@ -12,7 +11,7 @@ type Props = {
// props
const props = withDefaults(defineProps<Props>(), {
variant: "solid"
variant: "solid",
});
const { variant, message, error, disabled } = toRefs(props);
@@ -28,15 +27,15 @@ const inputRef = ref<HTMLInputElement | null>(null);
const classes = computed(() => {
return [
"flex items-center justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3 rounded-100",
"flex items-center text-black justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3.5 selection:bg-slate-100 rounded-100",
{
"input-solid": variant.value === "solid",
"input-outlined": variant.value === "outlined",
"input-effects": !error.value,
[variant.value === "solid"
? "input-solid-error"
: "input-outlined-error"]: error.value
}
: "input-outlined-error"]: error.value,
},
];
});
@@ -45,7 +44,6 @@ const classes = computed(() => {
const onInput = (e: any) => {
emit("update:modelValue", e.target.value);
};
</script>
<template>
@@ -56,7 +54,7 @@ const onInput = (e: any) => {
:value="modelValue"
@input="onInput"
ref="inputRef"
class="outline-none flex-1"
class="outline-none flex-1 text-sm placeholder-slate-400"
:placeholder="placeholder"
/>
@@ -28,7 +28,7 @@ const onSwiper = (swiper: SwiperClass) => {
<template>
<section class="w-full flex flex-col gap-[4rem] px-0 py-[5rem] container">
<div class="w-full flex justify-between items-center">
<span class="text-black typo-h-3">
<span class="text-black typo-h-4">
{{ title }}
</span>
<div class="flex-center gap-[.5rem]">
+16
View File
@@ -95,6 +95,22 @@ const nextPage = computed(() => route.meta.nextPage);
</span>
</div>
<label
v-if="route.name == 'cart-checkout'"
class="flex items-center w-full group gap-2 p-3 my-5 text-sm transition-all border text-gray-600 focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-cyan-500 bg-gray-50 border-gray-200 rounded-100"
>
<input
type="text"
placeholder="کد تخفیف"
class="w-full border-none focus:border-none focus:outline-none placeholder:text-gray-600 h-[22px]"
/>
<button
class="ring ring-offset-[-4px] active:ring-offset-2 transition-all duration-75 font-bold text-cyan-500 rounded-50"
>
ثبت
</button>
</label>
<NuxtLink :to="{ name: nextPage.name }">
<Button
start-icon="bi:arrow-right"
+103 -1
View File
@@ -1,14 +1,116 @@
<script setup lang="ts">
// meta
definePageMeta({
layout: "cart",
pageTitle: "ثبت سفارش",
prevPage: { name: "cart-delivery", label: "انتخاب آدرس" },
nextPage: { name: "checkout", label: "پرداخت" },
});
// types
type PaymentGateway = {
id: number;
picture: string;
title: string;
};
// state
const paymentGateways = ref<PaymentGateway[]>([
{
id: 1,
picture: "/zarinpal.png",
title: "زرین پال",
},
{
id: 2,
picture: "/saman-bank.png",
title: "بانک سامان",
},
{
id: 3,
picture: "/mellat-bank.png",
title: "بانک ملت",
},
{
id: 4,
picture: "/jibimo.png",
title: "جیبی مو",
},
{
id: 5,
picture: "/idpay.png",
title: "آی دی پی",
},
]);
const selectedGateway = ref<PaymentGateway>(paymentGateways.value[0]);
</script>
<template>
<div></div>
<div class="flex flex-col w-full gap-5">
<div
class="flex flex-col items-center w-full gap-4 p-4 border border-gray-300 rounded-xl bg-gray-50"
>
<span
class="flex items-center justify-start w-full text-[1.125rem] font-semibold text-gray-900"
>
روش پرداخت
</span>
<div
class="w-full grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4"
>
<div
v-for="(gateway, index) in paymentGateways"
@click="selectedGateway = { ...gateway }"
:key="index"
:class="
selectedGateway.id == gateway.id
? 'ring-2 ring-offset-2 ring-cyan-500 border-cyan-500'
: 'border-slate-200'
"
class="w-full p-5 border rounded-xl flex flex-col gap-4 transition-all cursor-pointer"
>
<div class="aspect-square flex-center">
<img :src="gateway.picture" class="object-cover" />
</div>
<span class="typo-label-sm text-black">
{{ gateway.title }}
</span>
</div>
</div>
</div>
<div
class="flex flex-col items-center w-full gap-4 p-4 border lg:gap-6 border-gray-300 rounded-xl bg-gray-50"
>
<span
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900"
>
خلاصه سفارش
</span>
<div
class="grid w-full grid-cols-1 gap-6 lg:grid-cols-3 md:grid-cols-2"
>
<MinimalCartItem v-for="i in 9" />
<div class="h-7 flex-center col-span-full lg:hidden">
<button class="gap-2 flex-center">
<span class="text-sm font-bold text-cyan-500">
مشاهده بیشتر
</span>
<Icon
name="bi:chevron-down"
class="**:stroke-cyan-500"
/>
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>
+165 -1
View File
@@ -1,14 +1,178 @@
<script setup lang="ts">
// meta
definePageMeta({
layout: "cart",
pageTitle: "انتخاب آدرس",
prevPage: { name: "cart", label: "سبد خرید" },
nextPage: { name: "cart-checkout", label: "تسویه حساب" },
});
// types
type DeliveryData = {
address: Address | null;
deliveryMethod: {
tipax: boolean;
pishtaz: boolean;
peyk: boolean;
};
};
// state
const addresses = ref<Address[]>([]);
const deliveryData = ref<DeliveryData>({
address: null,
deliveryMethod: {
peyk: false,
pishtaz: true,
tipax: false,
},
});
// methods
const handleAddNewAddress = (address: Address) => {
addresses.value.push(address);
};
const handleSelectAddress = (address: Address) => {
deliveryData.value.address = { ...address };
};
</script>
<template>
<div class="flex flex-col w-full gap-6"></div>
<div class="flex flex-col w-full gap-5">
<AddressItem @add="handleAddNewAddress" />
<div v-if="addresses.length > 0" class="flex flex-col w-full gap-6">
<span class="typo-sub-h-xl"> آدرس های شما </span>
<div class="flex flex-col gap-6 w-full">
<AddressItem
v-for="(address, index) in addresses"
:key="index"
:address="address"
@select="handleSelectAddress"
:isSelected="address.id == deliveryData.address?.id"
/>
</div>
</div>
<div
class="flex flex-col items-center w-full gap-4 p-4 border border-gray-300 rounded-xl bg-gray-50"
>
<span
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900"
>
زمان و شیوه ارسال
</span>
<label
@click="deliveryData.deliveryMethod.pishtaz = true"
:class="
deliveryData.deliveryMethod.pishtaz
? 'ring-cyan-500 ring-offset-2 ring-2'
: ''
"
class="flex flex-col select-none w-full gap-2 p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-cyan-500 ring-offset-2 focus-within:border-cyan-500 rounded-100 border-gray-300 bg-gray-50"
>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2.5">
<SwitchRoot
v-model="deliveryData.deliveryMethod.pishtaz"
:defaultValue="false"
class="w-[3rem] h-[1.8rem] shrink-0 flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-cyan-500 border border-slate-300 data-[state=checked]:border-cyan-800/20 rounded-full relative transition-all focus-within:outline-none"
>
<SwitchThumb
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
/>
</SwitchRoot>
<span
class="w-full text-gray-800 text-sm lg:text-[1rem]"
>پست پیشتاز</span
>
</div>
<span class="text-gray-800 text-sm lg:text-[1rem]">
۱۵۰٬۰۰۰ تومان
</span>
</div>
</label>
<label
class="flex items-center opacity-50 select-none pointer-events-none justify-between w-full p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-cyan-500 ring-offset-2 focus-within:border-cyan-500 rounded-100 border-gray-300 bg-gray-50"
>
<div class="flex items-center gap-2.5">
<SwitchRoot
v-model="deliveryData.deliveryMethod.tipax"
class="w-[3rem] h-[1.8rem] shrink-0 flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-cyan-500 border border-slate-300 data-[state=checked]:border-cyan-800/20 rounded-full relative transition-all focus-within:outline-none"
>
<SwitchThumb
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
/>
</SwitchRoot>
<span class="w-full text-gray-800 text-sm lg:text-[1rem]"
>تیپاکس</span
>
</div>
<span class="text-gray-800 text-sm lg:text-[1rem]">
۱۵۰٬۰۰۰ تومان
</span>
</label>
<label
class="flex items-center opacity-50 select-none pointer-events-none justify-between w-full p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-cyan-500 ring-offset-2 focus-within:border-cyan-500 rounded-100 border-gray-300 bg-gray-50"
>
<div class="flex items-center gap-2.5">
<SwitchRoot
v-model="deliveryData.deliveryMethod.peyk"
class="w-[3rem] h-[1.8rem] shrink-0 flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-cyan-500 border border-slate-300 data-[state=checked]:border-cyan-800/20 rounded-full relative transition-all focus-within:outline-none"
>
<SwitchThumb
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
/>
</SwitchRoot>
<span class="w-full text-gray-800 text-sm lg:text-[1rem]"
>ارسال با پیک (فقط ارسال درون شهری شیراز)</span
>
</div>
<span class="text-gray-800 text-sm lg:text-[1rem]">
۱۵۰٬۰۰۰ تومان
</span>
</label>
</div>
<div
class="flex flex-col items-center w-full gap-4 p-4 border lg:gap-6 border-gray-300 rounded-xl bg-gray-50"
>
<span
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900"
>
خلاصه سفارش
</span>
<div
class="grid w-full grid-cols-1 gap-6 lg:grid-cols-3 md:grid-cols-2"
>
<MinimalCartItem v-for="i in 9" />
<div class="h-7 flex-center col-span-full lg:hidden">
<button class="gap-2 flex-center">
<span class="text-sm font-bold text-cyan-500">
مشاهده بیشتر
</span>
<Icon
name="bi:chevron-down"
class="**:stroke-cyan-500"
/>
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

+43 -29
View File
@@ -9,38 +9,52 @@ declare global {
};
type Chat = {
id: number,
sender: "ai" | "user",
content: string
}
id: number;
sender: "ai" | "user";
content: string;
};
type Product = {
"id": number,
"price": number,
"name": string,
"description": string,
"currency": string,
"image": string,
"rating": number,
"view": number,
"sell": number,
"in_stock": number,
"discount": number,
"slug": string,
"meta_description": string | null,
"meta_keywords": null,
"meta_rating": number | null
}
id: number;
price: number;
name: string;
description: string;
currency: string;
image: string;
rating: number;
view: number;
sell: number;
in_stock: number;
discount: number;
slug: string;
meta_description: string | null;
meta_keywords: null;
meta_rating: number | null;
};
type Category = {
"id": number,
"name": string,
"slug": string,
"icon": string,
"meta_title": string,
"meta_description": string,
"parent": number,
"children": "string"
}
id: number;
name: string;
slug: string;
icon: string;
meta_title: string;
meta_description: string;
parent: number;
children: "string";
};
type Address = {
id: number;
province: string;
city: string;
postal_code: string;
full_address: string;
};
type DeliveryMethod = {
type: string;
date: string;
hour: string;
price: string;
};
}