Compare commits
10 Commits
38f5456290
...
84f49a75ae
| Author | SHA1 | Date | |
|---|---|---|---|
| 84f49a75ae | |||
| fa8ea8d8a9 | |||
| 3c1a082df3 | |||
| eddf95f609 | |||
| 761be9e66a | |||
| aea8fb8b3d | |||
| 632304c271 | |||
| 2d7b48b5b7 | |||
| 53484a07f8 | |||
| f355fd9bb6 |
@@ -16,6 +16,34 @@
|
||||
"main_title_line2": "سریع و مطمئن",
|
||||
"contact_us": "تماس با ما"
|
||||
},
|
||||
"Categories": {
|
||||
"title": "دسته بندی ها",
|
||||
"description": "از بین صدها مرکز زیبایی، دقیقا همونی که میخای رو پیدا کن."
|
||||
},
|
||||
"Salons": {
|
||||
"title": "سالن ها و کلینیک های زیبایی",
|
||||
"description": "از بین صدها مرکز، دقیقا همونی که میخای رو پیدا کن."
|
||||
},
|
||||
"App": {
|
||||
"title": "رزرو راحت تر با اپلیکیشن",
|
||||
"description": "با اپلیکیشن، سریع تر و راحت تر نوبت بگیر.",
|
||||
"download_app": "دانلود مستقیم",
|
||||
"download_google_play": "دانلود از گوگل پلی"
|
||||
},
|
||||
"Reservation": {
|
||||
"title": "رزرو خدمات زیبایی، ساده\u200Cتر از همیشه",
|
||||
"stations": "مراکز معتبر",
|
||||
"online_reserve": "رزرو انلاین سریع",
|
||||
"start_reserve": "شروع رزرو",
|
||||
"customer": "مشتریان",
|
||||
"customer_number": "+100K",
|
||||
"clinic": "کلینیک فعال",
|
||||
"clinic_number": "+336",
|
||||
"salon": "سالن زیبایی",
|
||||
"salon_number": "+345",
|
||||
"score": "امتیازدهی واقعی کاربران",
|
||||
"description": "ما این پلتفرم را ساختیم تا بتوانید بهترین سالن\u200Cهای زیبایی و کلینیک\u200Cهای معتبر را به راحتی پیدا کنید.\n مقایسه، رزرو آنلاین و تجربه\u200Cای بی\u200Cدردسر."
|
||||
},
|
||||
"Application": {
|
||||
"title": "دانلود نرم افزار تلفن همراه 141",
|
||||
"description": "نسخهی تلفن همراه اپلیکیشن مرکز مدیریت راههای کشور با قابلیتهایی نظیر نمایش تصاویر دوربینهای، نظارتی در سراسر کشور، مسیریابی همراه با نمایش مسافت و زمان تقریبی و جزئیات مسیر، مشاهده اخبار متنی و شنیداری، کروکی ترافیک استانهای کشور، دریافت اطلاعیههای مربوط به مسیرهای پرتردد و ... آماده دریافت از مارکتهای مختلف میباشد.",
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
"@tailwindcss/postcss": "^4.1.4",
|
||||
"@vis.gl/react-maplibre": "^8.0.4",
|
||||
"axios": "^1.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.9.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"maplibre-gl": "^5.4.0",
|
||||
@@ -35,6 +37,7 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^3.8.16",
|
||||
"postcss": "^8.5.3",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.3",
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "19.2.3",
|
||||
|
||||
1100
pnpm-lock.yaml
generated
3
public/icons/arrow-left.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="15" height="12" viewBox="0 0 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.06694 0.183058C6.31102 0.427136 6.31102 0.822864 6.06694 1.06694L2.13388 5H13.9583C14.3035 5 14.5833 5.27982 14.5833 5.625C14.5833 5.97018 14.3035 6.25 13.9583 6.25H2.13388L6.06694 10.1831C6.31102 10.4271 6.31102 10.8229 6.06694 11.0669C5.82286 11.311 5.42714 11.311 5.18306 11.0669L0.183058 6.06694C-0.0610194 5.82286 -0.0610194 5.42714 0.183058 5.18306L5.18306 0.183058C5.42714 -0.0610194 5.82286 -0.0610194 6.06694 0.183058Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 610 B |
3
public/icons/arrow-right.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2254 4.55806C11.4695 4.31398 11.8652 4.31398 12.1093 4.55806L17.1093 9.55806C17.3533 9.80214 17.3533 10.1979 17.1093 10.4419L12.1093 15.4419C11.8652 15.686 11.4695 15.686 11.2254 15.4419C10.9813 15.1979 10.9813 14.8021 11.2254 14.5581L15.1584 10.625H3.33398C2.98881 10.625 2.70898 10.3452 2.70898 10C2.70898 9.65482 2.98881 9.375 3.33398 9.375H15.1584L11.2254 5.44194C10.9813 5.19786 10.9813 4.80214 11.2254 4.55806Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 600 B |
3
public/icons/download.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99935 1.04102C10.3445 1.04102 10.6243 1.32084 10.6243 1.66602V10.8098L12.0248 9.17594C12.2495 8.91386 12.644 8.88351 12.9061 9.10815C13.1682 9.33279 13.1985 9.72735 12.9739 9.98943L10.4739 12.9061C10.3551 13.0446 10.1818 13.1243 9.99935 13.1243C9.8169 13.1243 9.64355 13.0446 9.52481 12.9061L7.02481 9.98943C6.80018 9.72735 6.83053 9.33279 7.09261 9.10815C7.35468 8.88351 7.74925 8.91386 7.97389 9.17594L9.37435 10.8098V1.66602C9.37435 1.32084 9.65417 1.04102 9.99935 1.04102ZM5.8292 6.87599C6.17438 6.87407 6.45575 7.15233 6.45767 7.4975C6.4596 7.84267 6.18133 8.12405 5.83616 8.12597C4.92491 8.13104 4.27901 8.1547 3.7888 8.24475C3.31646 8.33151 3.04307 8.47083 2.84019 8.6737C2.60956 8.90434 2.45918 9.22815 2.37697 9.83963C2.29234 10.4691 2.29102 11.3033 2.29102 12.4995V13.3329C2.29102 14.529 2.29234 15.3633 2.37697 15.9928C2.45918 16.6042 2.60956 16.9281 2.84019 17.1587C3.07083 17.3893 3.39464 17.5397 4.00612 17.6219C4.63558 17.7065 5.46984 17.7079 6.66602 17.7079H13.3327C14.5289 17.7079 15.3631 17.7065 15.9926 17.6219C16.6041 17.5397 16.9279 17.3893 17.1585 17.1587C17.3891 16.9281 17.5395 16.6042 17.6217 15.9928C17.7064 15.3633 17.7077 14.529 17.7077 13.3329V12.4995C17.7077 11.3033 17.7064 10.4691 17.6217 9.83963C17.5395 9.22815 17.3891 8.90434 17.1585 8.6737C16.9556 8.47083 16.6822 8.33151 16.2099 8.24475C15.7197 8.15471 15.0738 8.13104 14.1625 8.12597C13.8174 8.12405 13.5391 7.84267 13.541 7.4975C13.5429 7.15233 13.8243 6.87407 14.1695 6.87599C15.0708 6.881 15.8219 6.90257 16.4357 7.01532C17.0674 7.13135 17.605 7.35238 18.0424 7.78982C18.544 8.29142 18.7597 8.92298 18.8606 9.67307C18.9577 10.3955 18.9577 11.3141 18.9577 12.4538V13.3786C18.9577 14.5182 18.9577 15.4368 18.8606 16.1593C18.7597 16.9094 18.544 17.541 18.0424 18.0426C17.5408 18.5442 16.9092 18.7599 16.1591 18.8608C15.4367 18.9579 14.5181 18.9579 13.3784 18.9579H6.62029C5.48063 18.9579 4.56203 18.9579 3.83956 18.8608C3.08947 18.7599 2.4579 18.5442 1.95631 18.0426C1.45471 17.541 1.23897 16.9094 1.13812 16.1593C1.04099 15.4368 1.041 14.5182 1.04102 13.3786V12.4538C1.041 11.3141 1.04099 10.3955 1.13812 9.67307C1.23897 8.92298 1.45471 8.29142 1.95631 7.78982C2.39375 7.35238 2.93131 7.13134 3.56298 7.01532C4.17678 6.90257 4.92793 6.881 5.8292 6.87599Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
32
public/icons/google-play.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2324_35498)">
|
||||
<path d="M12.823 6.10533L0.755237 0.0771082C0.603595 0.000152232 0.412819 0.00442757 0.266069 0.0813835C0.114426 0.158339 0.0214844 0.29515 0.0214844 0.449062C0.0214844 0.449062 0.0263761 1.00486 0.0312677 1.91978L8.82162 9.60255L12.823 6.10533Z" fill="url(#paint0_linear_2324_35498)"/>
|
||||
<path d="M0.03125 1.91992C0.0459251 5.22048 0.0899502 13.2324 0.109517 17.2213L8.8265 9.60269L0.03125 1.91992Z" fill="url(#paint1_linear_2324_35498)"/>
|
||||
<path d="M19.7298 9.55566L12.8227 6.10547L8.81641 9.60269L13.3363 13.5531L19.7346 10.2953C19.8863 10.2183 19.9792 10.0773 19.9792 9.92761C19.9792 9.7737 19.8814 9.63262 19.7298 9.55566Z" fill="url(#paint2_linear_2324_35498)"/>
|
||||
<path d="M0.103516 17.2218C0.113299 18.6455 0.118191 19.5561 0.118191 19.5561C0.118191 19.71 0.211133 19.8511 0.362775 19.9238C0.514417 20.0008 0.700301 20.0008 0.851943 19.9238L13.3404 13.5578L8.82049 9.60742L0.103516 17.2218Z" fill="url(#paint3_linear_2324_35498)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2324_35498" x1="0.0214844" y1="4.81392" x2="12.8253" y2="4.81392" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#63BE6B"/>
|
||||
<stop offset="0.506" stop-color="#5BBC6A"/>
|
||||
<stop offset="1" stop-color="#4AB96A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2324_35498" x1="0.028902" y1="9.57247" x2="8.82141" y2="9.57247" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3EC6F2"/>
|
||||
<stop offset="1" stop-color="#45AFE3"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2324_35498" x1="8.8211" y1="9.83202" x2="19.9781" y2="9.83202" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAA51A"/>
|
||||
<stop offset="0.387" stop-color="#FAB716"/>
|
||||
<stop offset="0.741" stop-color="#FAC412"/>
|
||||
<stop offset="1" stop-color="#FAC80F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_2324_35498" x1="0.105668" y1="14.7943" x2="13.3402" y2="14.7943" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EC3B50"/>
|
||||
<stop offset="1" stop-color="#E7515B"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2324_35498">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
12
public/icons/qrcode.svg
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
public/images/Qrcode-sample.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
public/images/app-section/ellipse.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/images/app-section/mobile1.png
Normal file
|
After Width: | Height: | Size: 312 KiB |
BIN
public/images/app-section/mobile2.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
public/images/categories/فیشیال.jpg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
public/images/categories/مو.jpg
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
public/images/categories/مژه.jpg
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
public/images/categories/میکاپ.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/images/categories/ناخن.jpg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/images/categories/ژل.jpg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
public/images/reservation/reservation.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/images/salons/salon.jpg
Normal file
|
After Width: | Height: | Size: 256 KiB |
@@ -17,3 +17,8 @@ export { default as CalenderIcon } from "&/icons/calender.svg";
|
||||
export { default as LocationIcon } from "&/icons/location.svg";
|
||||
export { default as MarkerIcon } from "&/icons/marker-icon.svg";
|
||||
export { default as MarkerPreviewIcon } from "&/icons/marker-preview.svg";
|
||||
export { default as ArrowRight } from "&/icons/arrow-right.svg";
|
||||
export { default as ArrowLeft } from "&/icons/arrow-left.svg";
|
||||
export { default as DownloadIcon } from "&/icons/download.svg";
|
||||
export { default as GooglePlayDownloadIcon } from "&/icons/google-play.svg";
|
||||
export { default as QrcodeIcon } from "&/icons/qrcode.svg";
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function SearchBar() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openModal(<LocationModal />)}
|
||||
className="w-full text-right text-gray-500"
|
||||
className="w-full text-right text-text-primary"
|
||||
>
|
||||
آدرس
|
||||
</button>
|
||||
|
||||
@@ -18,28 +18,28 @@ export default function BannerComponent() {
|
||||
initial={{ opacity: 0, y: 60 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.9, ease: "easeOut" }}
|
||||
className="bg-home-page-banner relative mx-16 mt-16 rounded-tr-[200px] rounded-bl-[200px] px-2 pt-2"
|
||||
className="bg-home-page-banner relative mx-16 mt-16 md:rounded-tr-[100px] md:rounded-bl-[80px] lg:rounded-tr-[150px] lg:rounded-bl-[150px] px-2 pt-2"
|
||||
>
|
||||
{/* ================= Header ================= */}
|
||||
<header className="flex w-full items-center justify-center pt-8">
|
||||
<div className="flex w-full items-center justify-center space-x-5">
|
||||
<header className="flex w-full items-center justify-center pt-8 px-5">
|
||||
<div className="flex w-full items-center justify-start space-x-5">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", stiffness: 200 }}
|
||||
>
|
||||
<LogoIcon className="size-12" />
|
||||
<LogoIcon className="size-8 lg:size-10" />
|
||||
</motion.div>
|
||||
|
||||
<ThemeToggle />
|
||||
|
||||
{["home", "search", "salons", "about_us", "contact_us"].map((item, index) => (
|
||||
{["home", "salons", "about_us", "contact_us"].map((item, index) => (
|
||||
<motion.p
|
||||
key={item}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 * index }}
|
||||
className="text-text-primary hover:text-primary-200 cursor-pointer transition"
|
||||
className="text-text-primary text-center hover:text-primary-200 cursor-pointer transition text-xs lg:text-sm"
|
||||
>
|
||||
{t(item)}
|
||||
</motion.p>
|
||||
@@ -49,17 +49,17 @@ export default function BannerComponent() {
|
||||
<div className="flex w-full items-center justify-center">
|
||||
{isAuth ? (
|
||||
<motion.div whileHover={{ scale: 1.1 }} className="cursor-pointer">
|
||||
<AvatarIcon className="h-6 w-6" />
|
||||
<AvatarIcon className="size-4 lg:size-5" />
|
||||
</motion.div>
|
||||
) : (
|
||||
<span className="flex w-full items-center justify-center gap-x-2">
|
||||
<span className="flex w-full items-center justify-end gap-x-2">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="bg-bg flex items-center justify-center gap-x-2 rounded-lg p-3"
|
||||
>
|
||||
<MyOrderIcon className="h-6 w-6" />
|
||||
<p>{t("my_order")}</p>
|
||||
<MyOrderIcon className="size-5 lg:size-6" />
|
||||
<p className="text-xs lg:text-base">{t("my_order")}</p>
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
@@ -67,8 +67,8 @@ export default function BannerComponent() {
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="bg-primary-200 flex items-center justify-center gap-x-2 rounded-lg p-3"
|
||||
>
|
||||
<AvatarIcon className="h-6 w-6" />
|
||||
<p>{t("auth")}</p>
|
||||
<AvatarIcon className="size-4 lg:size-5" />
|
||||
<p className="text-xs lg:text-base">{t("auth")}</p>
|
||||
</motion.button>
|
||||
</span>
|
||||
)}
|
||||
@@ -80,7 +80,7 @@ export default function BannerComponent() {
|
||||
{/* ===== Left Content ===== */}
|
||||
<div className="flex w-full flex-col items-center justify-center space-y-7">
|
||||
{/* Title */}
|
||||
<motion.div className="text-4xl font-bold">
|
||||
<motion.div className="text-2xl lg:text-4xl font-bold">
|
||||
<motion.span
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
@@ -124,7 +124,7 @@ export default function BannerComponent() {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
}}
|
||||
className="text-center text-xs"
|
||||
className="text-center px-10 text-xs"
|
||||
>
|
||||
{t("des")}
|
||||
</motion.p>
|
||||
@@ -147,10 +147,17 @@ export default function BannerComponent() {
|
||||
|
||||
{/* ===== Right Image ===== */}
|
||||
<motion.div
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
|
||||
className="flex w-full items-center justify-center"
|
||||
// animate={{ y: [0, -12, 0] }}
|
||||
// transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
|
||||
className="flex relative w-full items-center justify-center"
|
||||
>
|
||||
<motion.div
|
||||
className="w-50 sm:w-52 md:w-78 lg:w-90 absolute -bottom-50 left-10"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
whileInView={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.4, duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<Image
|
||||
src={BannerImage}
|
||||
alt="main_image"
|
||||
@@ -159,9 +166,10 @@ export default function BannerComponent() {
|
||||
height={200}
|
||||
priority
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.main>
|
||||
<div className="absolute -bottom-10 left-24">
|
||||
<div className="absolute -bottom-10 left-10 lg:left-24">
|
||||
<SearchBar />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
123
src/components/Home/Desktop/Categories/CarouselComponent.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent } from "@/components/UI/card";
|
||||
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/UI/Carousel";
|
||||
import { categories } from "@/data/mockCategories";
|
||||
import Image from "next/image";
|
||||
import { easeOut, motion } from "framer-motion";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
show: {
|
||||
transition: {
|
||||
staggerChildren: 0.08,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 30,
|
||||
scale: 0.95,
|
||||
},
|
||||
show: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: easeOut,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function CarouselComponent({ title, description }: Props) {
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Carousel dir="rtl" className="w-full" opts={{ direction: "rtl" }}>
|
||||
{/* 🔥 Animated Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 40 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="my-5 flex h-full w-full items-center justify-between"
|
||||
>
|
||||
<span className="flex items-center justify-center gap-x-2">
|
||||
<motion.div
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: 64 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="bg-primary-100 w-4 rounded-2xl rounded-tr-none"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-lg font-semibold">{title}</p>
|
||||
<p className="text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
{/* 🔥 Animated Carousel Items */}
|
||||
<motion.div variants={containerVariants} initial="hidden" animate="show">
|
||||
<CarouselContent>
|
||||
{categories.map((category) => (
|
||||
<CarouselItem
|
||||
key={category.id}
|
||||
className="after:bg-border relative after:absolute after:top-1/2 after:left-2 after:h-[50%] after:w-px after:-translate-y-1/2 after:content-[''] last:after:hidden md:basis-1/3 lg:basis-1/6"
|
||||
>
|
||||
<motion.div variants={itemVariants} className="p-1">
|
||||
<motion.div
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
y: -5,
|
||||
}}
|
||||
transition={{ type: "spring", stiffness: 300 }}
|
||||
>
|
||||
<Card dir="rtl" className="cursor-pointer transition-shadow hover:shadow-xl">
|
||||
<CardContent className="flex h-30 flex-col items-center justify-center gap-3 p-6">
|
||||
<motion.div
|
||||
whileHover={{ rotate: 5 }}
|
||||
transition={{ type: "spring", stiffness: 200 }}
|
||||
>
|
||||
<Image
|
||||
src={category.image}
|
||||
alt={category.title}
|
||||
width={80}
|
||||
height={80}
|
||||
className="rounded-full"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<p className="text-text-muted font-semibold">{category.title}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
</motion.div>
|
||||
|
||||
{/* 🔥 Animated Controls */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="absolute top-10 left-16 flex gap-2"
|
||||
>
|
||||
<motion.div whileTap={{ scale: 0.85 }}>
|
||||
<CarouselPrevious />
|
||||
</motion.div>
|
||||
<motion.div whileTap={{ scale: 0.85 }}>
|
||||
<CarouselNext />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CarouselComponent } from "./CarouselComponent";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const Categories = () => {
|
||||
const t = useTranslations("Categories");
|
||||
|
||||
return (
|
||||
<div className="container mx-auto my-16">
|
||||
<CarouselComponent title={t("title")} description={t("description")} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Categories;
|
||||
|
||||
95
src/components/Home/Desktop/Reservation/index.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ArrowTopLeft } from "@/assets";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Reservation() {
|
||||
const t = useTranslations("Reservation");
|
||||
|
||||
return (
|
||||
<section className="my-32 w-full lg:my-40">
|
||||
<div className="container mx-auto px-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 80 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="bg-primary-50 relative grid grid-cols-2 items-center gap-16 rounded-tr-[60px] rounded-bl-[60px] p-8 md:rounded-tr-[80px] md:rounded-bl-[80px] md:px-12"
|
||||
>
|
||||
{/* ================= TEXT CONTENT ================= */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -40 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.3, duration: 0.7 }}
|
||||
className="order-1 w-full space-y-8 text-white lg:px-10"
|
||||
>
|
||||
<h2 className="text-xl leading-relaxed font-bold md:text-2xl lg:text-3xl">{t("title")}</h2>
|
||||
|
||||
<p className="text-primary-100 max-w-xl text-sm leading-8 md:text-base">{t("description")}</p>
|
||||
|
||||
{/* Features */}
|
||||
<div className="flex flex-wrap items-center gap-x-8 gap-y-4 pt-2 text-sm">
|
||||
{[t("stations"), t("online_reserve"), t("score")].map((item, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<span className="bg-primary-200 h-3 w-3 rounded-full" />
|
||||
<span className="text-xs md:text-sm">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Button */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.07 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="text-primary-100 mt-4 flex w-fit items-center gap-2 rounded-xl bg-white px-6 py-3 font-medium shadow-lg"
|
||||
>
|
||||
{t("start_reserve")}
|
||||
<ArrowTopLeft className="size-5" />
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
|
||||
{/* ================= IMAGE + STATS ================= */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 60 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.4, duration: 0.9 }}
|
||||
className="relative order-2 flex flex-col items-center"
|
||||
>
|
||||
{/* Image */}
|
||||
<div className="relative h-[320px] w-[260px] overflow-hidden rounded-full border-8 border-white shadow-2xl md:h-[420px] md:w-[320px] lg:h-[580px] lg:w-[450px]">
|
||||
<Image
|
||||
src="/images/reservation/reservation.jpg"
|
||||
alt="reservation"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stats Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2, duration: 0.6 }}
|
||||
className="bg-card mt-8 flex w-full max-w-md items-center justify-between gap-6 rounded-[40px] px-6 py-6 shadow-2xl md:rounded-[60px] lg:absolute lg:-bottom-16"
|
||||
>
|
||||
{[
|
||||
{ number: t("customer_number"), label: t("customer") },
|
||||
{ number: t("clinic_number"), label: t("clinic") },
|
||||
{ number: t("salon_number"), label: t("salon") },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex-1 text-center">
|
||||
<p className="text-text-primary text-lg font-bold md:text-xl">{item.number}</p>
|
||||
<p className="text-text-muted mt-1 text-xs md:text-sm">{item.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
124
src/components/Home/Desktop/Salons/CarouselComponent.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent } from "@/components/UI/card";
|
||||
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/UI/Carousel";
|
||||
import { salons } from "@/data/mockCategories";
|
||||
import Image from "next/image";
|
||||
import { easeOut, motion } from "framer-motion";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
show: {
|
||||
transition: {
|
||||
staggerChildren: 0.08,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 30,
|
||||
scale: 0.95,
|
||||
},
|
||||
show: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: easeOut,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function CarouselComponent({ title, description }: Props) {
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Carousel dir="rtl" className="w-full" opts={{ direction: "rtl" }}>
|
||||
{/* 🔥 Animated Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 40 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="my-5 flex h-full w-full items-center justify-between"
|
||||
>
|
||||
<span className="flex items-center justify-center gap-x-2">
|
||||
<motion.div
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: 64 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="bg-primary-100 w-4 rounded-2xl rounded-tr-none"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-lg font-semibold">{title}</p>
|
||||
<p className="text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
{/* 🔥 Animated Carousel Items */}
|
||||
<motion.div variants={containerVariants} initial="hidden" animate="show">
|
||||
<CarouselContent>
|
||||
{salons.map((category) => (
|
||||
<CarouselItem
|
||||
key={category.id}
|
||||
className="after:bg-border relative after:absolute after:top-1/2 after:left-2 after:h-[60%] after:w-px after:-translate-y-1/2 after:content-[''] last:after:hidden md:basis-1/3 lg:basis-1/5"
|
||||
>
|
||||
<motion.div variants={itemVariants} className="p-1">
|
||||
<motion.div
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
y: -5,
|
||||
}}
|
||||
transition={{ type: "spring", stiffness: 300 }}
|
||||
>
|
||||
<Card dir="rtl" className="cursor-pointer transition-shadow hover:shadow-xl">
|
||||
<CardContent className="flex h-30 flex-col items-center justify-center gap-3">
|
||||
<motion.div
|
||||
whileHover={{ rotate: 5 }}
|
||||
transition={{ type: "spring", stiffness: 200 }}
|
||||
className="h-full"
|
||||
>
|
||||
<Image
|
||||
src={category.image}
|
||||
alt={category.salon_name}
|
||||
width={200}
|
||||
height={500}
|
||||
className="rounded-xl"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<p className="text-text-muted font-semibold">{category.salon_name}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
</motion.div>
|
||||
|
||||
{/* 🔥 Animated Controls */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="absolute top-10 left-16 flex gap-2"
|
||||
>
|
||||
<motion.div whileTap={{ scale: 0.85 }}>
|
||||
<CarouselPrevious />
|
||||
</motion.div>
|
||||
<motion.div whileTap={{ scale: 0.85 }}>
|
||||
<CarouselNext />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
src/components/Home/Desktop/Salons/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { CarouselComponent } from "./CarouselComponent";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const Salons = () => {
|
||||
const t = useTranslations("Salons");
|
||||
|
||||
return (
|
||||
<div className="container mx-auto my-16">
|
||||
<CarouselComponent title={t("title")} description={t("description")} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Salons;
|
||||
102
src/components/Home/Desktop/app/index.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import { motion } from "framer-motion";
|
||||
import { DownloadIcon, GooglePlayDownloadIcon } from "@/assets";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function App() {
|
||||
const t = useTranslations("App");
|
||||
return (
|
||||
<div className="bg-primary-500 relative container mx-auto mt-32 mb-16 w-full rounded-tl-[100px] rounded-br-[100px] p-8">
|
||||
<section className="grid grid-cols-2 gap-5">
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<section className="flex w-full flex-col items-center justify-center gap-2">
|
||||
<p className="text-text-primary text-lg font-semibold lg:text-2xl">{t("title")}</p>
|
||||
<p className="text-text-muted text-xs lg:text-sm">{t("description")}</p>
|
||||
<span className="mt-4 flex w-full items-center justify-center gap-x-2">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="bg-primary-200 flex items-center justify-center gap-x-2 rounded-lg p-3"
|
||||
>
|
||||
<DownloadIcon className="size-4 text-white lg:size-6" />
|
||||
<p className="text-xs text-white lg:text-base">{t("download_app")}</p>
|
||||
</motion.button>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="bg-bg flex items-center justify-center gap-x-2 rounded-lg p-3"
|
||||
>
|
||||
<GooglePlayDownloadIcon className="size-4 lg:size-6" />
|
||||
<p className="text-text-primary text-xs lg:text-base">{t("download_google_play")}</p>
|
||||
</motion.button>
|
||||
</span>
|
||||
</section>
|
||||
<div className="md:hidden lg:block">
|
||||
<Image
|
||||
src={"/images/Qrcode-sample.jpg"}
|
||||
alt={"Qrcode sample..."}
|
||||
width={200}
|
||||
height={200}
|
||||
className="rounded-3xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-10 w-fit lg:left-20">
|
||||
<motion.div
|
||||
className="w-50 sm:w-52 md:w-80 lg:w-90"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
whileInView={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.4, duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<Image
|
||||
src={"/images/app-section/ellipse.jpg"}
|
||||
alt={"app ellipse..."}
|
||||
width={500}
|
||||
height={200}
|
||||
className="rounded-t-full object-cover"
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="absolute -top-40 left-20 flex"
|
||||
initial={{ opacity: 0, x: -100 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.2, duration: 0.8, ease: "easeOut" }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<motion.div
|
||||
className="w-50 sm:w-52 md:w-60 lg:w-80"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
whileInView={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.4, duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<Image
|
||||
src={"/images/app-section/mobile1.png"}
|
||||
alt="گوشی سفید"
|
||||
width={500}
|
||||
height={500}
|
||||
className="h-auto w-full"
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="absolute top-5 -left-30 z-10 w-50 sm:w-52 md:w-90 lg:w-110"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
whileInView={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.4, duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<Image
|
||||
src={"/images/app-section/mobile2.png"}
|
||||
alt="گوشی مشکی"
|
||||
width={500}
|
||||
height={500}
|
||||
className="h-auto w-full"
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,17 @@
|
||||
import BannerComponent from "./Banner";
|
||||
import Categories from "./Categories";
|
||||
import Reservation from "./Reservation";
|
||||
import Salons from "./Salons";
|
||||
import App from "@/components/Home/Desktop/app";
|
||||
|
||||
export default function DesktopHome() {
|
||||
return (
|
||||
<div className="container mx-auto h-full w-full">
|
||||
<BannerComponent />
|
||||
<Categories />
|
||||
<Reservation />
|
||||
<Salons />
|
||||
<App />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import { MarkerIcon, MarkerPreviewIcon } from "@/assets";
|
||||
import { useMap } from "@vis.gl/react-maplibre";
|
||||
import maplibregl from "maplibre-gl";
|
||||
@@ -8,7 +9,7 @@ const createMarkerElement = (preview: boolean) => {
|
||||
const el = document.createElement("div");
|
||||
const Icon = preview ? MarkerPreviewIcon : MarkerIcon;
|
||||
|
||||
el.innerHTML = renderToStaticMarkup(<Icon className="text-neo-aqua size-10" />);
|
||||
el.innerHTML = renderToStaticMarkup(<Icon className="text-primary-100 size-10" />);
|
||||
|
||||
return el;
|
||||
};
|
||||
@@ -19,7 +20,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const MarkerPosition = ({ preview = false, onChange }: Props) => {
|
||||
const { homePageMap } = useMap();
|
||||
const { current: homePageMap } = useMap();
|
||||
const markerRef = useRef<maplibregl.Marker | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,10 +50,10 @@ const MarkerPosition = ({ preview = false, onChange }: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
map.on("moveend", onMove);
|
||||
map.on("move", onMove);
|
||||
|
||||
return () => {
|
||||
map.off("moveend", onMove);
|
||||
map.off("move", onMove);
|
||||
markerRef.current?.remove();
|
||||
markerRef.current = null;
|
||||
};
|
||||
|
||||
227
src/components/UI/Carousel.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/UI/button";
|
||||
import { ArrowLeft, ArrowRight, ChevronIcon } from "@/assets";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return;
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return;
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return;
|
||||
onSelect(api);
|
||||
api.on("reInit", onSelect);
|
||||
api.on("select", onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="border-text-primary/10 overflow-hidden rounded-2xl border p-1"
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon-sm",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-previous"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"bg-card border-primary-300 absolute touch-manipulation rounded-xl border p-5",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="text-text-primary" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon-sm",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-next"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"bg-card border-primary-300 absolute touch-manipulation rounded-xl p-5",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="text-text-primary size-5" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext, useCarousel };
|
||||
64
src/components/UI/button.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Slot } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||
destructive:
|
||||
"bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
73
src/components/UI/card.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({ className, size = "default", ...props }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"ring-foreground/10 text-card-foreground group/card flex flex-col gap-4 overflow-hidden rounded-xl py-4 text-sm has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} />;
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return <div data-slot="card-content" className={cn("px-4 group-data-[size=sm]/card:px-3", className)} {...props} />;
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 flex items-center rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
|
||||
103
src/data/mockCategories.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
export const categories = [
|
||||
{
|
||||
id: 1,
|
||||
title: "خدمات مو",
|
||||
image: "/images/categories/مو.jpg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "خدمات میکاپ",
|
||||
image: "/images/categories/میکاپ.jpg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "خدمات ناخن",
|
||||
image: "/images/categories/ناخن.jpg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "خدمات فیشیال",
|
||||
image: "/images/categories/فیشیال.jpg",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "خدمات مژه و ابرو",
|
||||
image: "/images/categories/مژه.jpg",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "ژل و بوتاکس",
|
||||
image: "/images/categories/ژل.jpg",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: "ژل و بوتاکس",
|
||||
image: "/images/categories/ژل.jpg",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: "ژل و بوتاکس",
|
||||
image: "/images/categories/ژل.jpg",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: "ژل و بوتاکس",
|
||||
image: "/images/categories/ژل.jpg",
|
||||
},
|
||||
];
|
||||
export const salons = [
|
||||
{
|
||||
id: 1,
|
||||
salon_name: "سالن همارو",
|
||||
rate: "4",
|
||||
image: "/images/salons/salon.jpg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
salon_name: "سالن بیوتی",
|
||||
rate: "4",
|
||||
image: "/images/salons/salon.jpg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
rate: "4",
|
||||
salon_name: "سالن عروس",
|
||||
image: "/images/salons/salon.jpg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
rate: "4",
|
||||
salon_name: "سالن فیشیال",
|
||||
image: "/images/salons/salon.jpg",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
rate: "4",
|
||||
salon_name: "سالن زیبایی",
|
||||
image: "/images/salons/salon.jpg",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
rate: "4",
|
||||
salon_name: "سالن همارو",
|
||||
image: "/images/salons/salon.jpg",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
rate: "4",
|
||||
salon_name: "سالن عروس",
|
||||
image: "/images/salons/salon.jpg",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
rate: "4",
|
||||
salon_name: "سالن بیوتی",
|
||||
image: "/images/salons/salon.jpg",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
rate: "4",
|
||||
salon_name: "سالن بیوتی",
|
||||
image: "/images/salons/salon.jpg",
|
||||
},
|
||||
];
|
||||
@@ -131,6 +131,7 @@ body {
|
||||
/* =====================
|
||||
Primary (Purple)
|
||||
====================== */
|
||||
--color-primary-50: #140b4e;
|
||||
--color-primary-100: #7d34b9;
|
||||
--color-primary-200: #9a5cff;
|
||||
--color-primary-300: #b38bff;
|
||||
@@ -191,6 +192,7 @@ body {
|
||||
--color-text-muted: #9ca3af;
|
||||
|
||||
--color-primary-100: #b38bff;
|
||||
--color-primary-50: #140b4e;
|
||||
--color-primary-200: #9a5cff;
|
||||
--color-primary-300: #7d34b9;
|
||||
|
||||
|
||||