Compare commits

...

14 Commits

Author SHA1 Message Date
84f49a75ae change style 2026-05-07 10:17:47 +03:30
fa8ea8d8a9 build and format 2026-02-27 11:17:48 +03:30
3c1a082df3 add app section 2026-02-27 11:16:28 +03:30
eddf95f609 build and format 2026-02-26 11:06:48 +03:30
761be9e66a salons section 2026-02-26 11:06:05 +03:30
aea8fb8b3d build and format 2026-02-26 10:42:38 +03:30
632304c271 reservation style 2026-02-26 10:41:35 +03:30
2d7b48b5b7 build and format 2026-02-17 21:36:14 +03:30
53484a07f8 add category section 2026-02-17 21:33:47 +03:30
f355fd9bb6 add carousel 2026-02-08 16:11:13 +03:30
38f5456290 build and format 2026-02-06 10:04:47 +03:30
dc0a04dc7d banner of desktop completed 2026-02-06 10:03:55 +03:30
f44e3ab421 build and format 2026-02-05 12:35:26 +03:30
cf51b14681 add banner of home page 2026-02-05 12:32:20 +03:30
67 changed files with 15697 additions and 12089 deletions

View File

@@ -4,10 +4,45 @@
"Header": {},
"HomePage": {
"title": "سلام",
"about141": "درباره 141",
"blog": "وبلاگ",
"searchThisArea": "جست و جو در این محدوده",
"allRightsReserved": "تمامی حقوق این وبسایت متعلق به مرکز مدیریت راه‌های کشور می‌باشد."
"home": "خانه",
"search": "جست و جو",
"salons": "سالن ها",
"see_salons": "مشاهده سالن ها",
"about_us": "درباره ما",
"my_order": "سفارش های من",
"auth": "ورود و ثبت نام",
"des": "بهترین سالن های زیبایی و کلینیک ها را ببین ، مقایسه کن و انلاین نوبت بگیر",
"main_title_line1": "رزرو آنلاین خدمات زیبایی،",
"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",

View File

@@ -3,5 +3,21 @@ import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin();
const nextConfig: NextConfig = {};
const nextConfig: NextConfig = {
turbopack: {
rules: {
"*.svg": {
loaders: [
{
loader: "@svgr/webpack",
options: {
icon: true,
},
},
],
as: "*.ts",
},
},
},
};
export default withNextIntl(nextConfig);

View File

@@ -1,60 +1,66 @@
{
"name": "salonro",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"format": "npx prettier . --write"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@googlemaps/polyline-codec": "^1.0.28",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/postcss": "^4.1.4",
"@vis.gl/react-maplibre": "^8.0.4",
"axios": "^1.10.0",
"clsx": "^2.1.1",
"framer-motion": "^12.9.2",
"js-cookie": "^3.0.5",
"maplibre-gl": "^5.4.0",
"next": "16.1.1",
"next-intl": "^4.6.1",
"next-pwa": "^5.6.0",
"next-themes": "^0.4.6",
"nextjs-toploader": "^3.8.16",
"postcss": "^8.5.3",
"react": "19.2.3",
"react-day-picker": "^9.13.0",
"react-dom": "19.2.3",
"react-hot-toast": "^2.6.0",
"tailwind-merge": "^3.2.0",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.1.4",
"zustand": "^5.0.9"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/js-cookie": "^3.0.6",
"@types/next-pwa": "^5.6.9",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.1",
"prettier": "3.5.3",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4",
"typescript": "^5"
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
}
{
"name": "salonro",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"format": "npx prettier . --write"
},
"dependencies": {
"@base-ui/react": "^1.1.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@googlemaps/polyline-codec": "^1.0.28",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@svgr/webpack": "^8.1.0",
"@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",
"next": "16.1.1",
"next-intl": "^4.6.1",
"next-pwa": "^5.6.0",
"next-sitemap": "^4.2.3",
"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",
"react-hook-form": "^7.71.1",
"react-hot-toast": "^2.6.0",
"tailwind-merge": "^3.2.0",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.1.4",
"zustand": "^5.0.9"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/js-cookie": "^3.0.6",
"@types/next-pwa": "^5.6.9",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.1",
"prettier": "3.5.3",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4",
"typescript": "^5"
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
}

25245
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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 d="M15.4419 14.5581C15.686 14.8021 15.686 15.1979 15.4419 15.4419C15.1979 15.686 14.8021 15.686 14.5581 15.4419L8.75 9.63388L5.44194 12.9419C5.26319 13.1207 4.99437 13.1742 4.76082 13.0774C4.52728 12.9807 4.375 12.7528 4.375 12.5V5C4.375 4.65482 4.65482 4.375 5 4.375L12.5 4.375C12.7528 4.375 12.9807 4.52728 13.0774 4.76082C13.1742 4.99437 13.1207 5.26319 12.9419 5.44194L9.63388 8.75L15.4419 14.5581Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 539 B

View File

@@ -0,0 +1,9 @@
<svg width="195" height="2" viewBox="0 0 195 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="195" height="2" rx="1" fill="url(#paint0_linear_1658_15851)"/>
<defs>
<linearGradient id="paint0_linear_1658_15851" x1="0" y1="1" x2="195" y2="1" gradientUnits="userSpaceOnUse">
<stop stop-color="#9480DF"/>
<stop offset="1" stop-color="#9480DF" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0L13.3262 3.58401C14.5416 6.86864 17.1314 9.45837 20.416 10.6738L24 12L20.416 13.3262C17.1314 14.5416 14.5416 17.1314 13.3262 20.416L12 24L10.6738 20.416C9.45837 17.1314 6.86864 14.5416 3.58401 13.3262L0 12L3.58401 10.6738C6.86864 9.45837 9.45837 6.86864 10.6738 3.58401L12 0Z" fill="#9480DF"/>
</svg>

After

Width:  |  Height:  |  Size: 415 B

View File

@@ -0,0 +1,9 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 14C17.5523 14 18 13.5523 18 13C18 12.4477 17.5523 12 17 12C16.4477 12 16 12.4477 16 13C16 13.5523 16.4477 14 17 14Z" fill="currentColor"/>
<path d="M17 18C17.5523 18 18 17.5523 18 17C18 16.4477 17.5523 16 17 16C16.4477 16 16 16.4477 16 17C16 17.5523 16.4477 18 17 18Z" fill="currentColor"/>
<path d="M13 13C13 13.5523 12.5523 14 12 14C11.4477 14 11 13.5523 11 13C11 12.4477 11.4477 12 12 12C12.5523 12 13 12.4477 13 13Z" fill="currentColor"/>
<path d="M13 17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17C11 16.4477 11.4477 16 12 16C12.5523 16 13 16.4477 13 17Z" fill="currentColor"/>
<path d="M7 14C7.55229 14 8 13.5523 8 13C8 12.4477 7.55229 12 7 12C6.44772 12 6 12.4477 6 13C6 13.5523 6.44772 14 7 14Z" fill="currentColor"/>
<path d="M7 18C7.55229 18 8 17.5523 8 17C8 16.4477 7.55229 16 7 16C6.44772 16 6 16.4477 6 17C6 17.5523 6.44772 18 7 18Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.75C7.41421 1.75 7.75 2.08579 7.75 2.5V3.26272C8.412 3.24999 9.14133 3.24999 9.94346 3.25H14.0564C14.8586 3.24999 15.588 3.24999 16.25 3.26272V2.5C16.25 2.08579 16.5858 1.75 17 1.75C17.4142 1.75 17.75 2.08579 17.75 2.5V3.32709C18.0099 3.34691 18.2561 3.37182 18.489 3.40313C19.6614 3.56076 20.6104 3.89288 21.3588 4.64124C22.1071 5.38961 22.4392 6.33855 22.5969 7.51098C22.75 8.65018 22.75 10.1058 22.75 11.9435V14.0564C22.75 15.8941 22.75 17.3498 22.5969 18.489C22.4392 19.6614 22.1071 20.6104 21.3588 21.3588C20.6104 22.1071 19.6614 22.4392 18.489 22.5969C17.3498 22.75 15.8942 22.75 14.0565 22.75H9.94359C8.10585 22.75 6.65018 22.75 5.51098 22.5969C4.33856 22.4392 3.38961 22.1071 2.64124 21.3588C1.89288 20.6104 1.56076 19.6614 1.40314 18.489C1.24997 17.3498 1.24998 15.8942 1.25 14.0564V11.9436C1.24998 10.1058 1.24997 8.65019 1.40314 7.51098C1.56076 6.33855 1.89288 5.38961 2.64124 4.64124C3.38961 3.89288 4.33856 3.56076 5.51098 3.40313C5.7439 3.37182 5.99006 3.34691 6.25 3.32709V2.5C6.25 2.08579 6.58579 1.75 7 1.75ZM5.71085 4.88976C4.70476 5.02502 4.12511 5.27869 3.7019 5.7019C3.27869 6.12511 3.02502 6.70476 2.88976 7.71085C2.86685 7.88123 2.8477 8.06061 2.83168 8.25H21.1683C21.1523 8.06061 21.1331 7.88124 21.1102 7.71085C20.975 6.70476 20.7213 6.12511 20.2981 5.7019C19.8749 5.27869 19.2952 5.02502 18.2892 4.88976C17.2615 4.75159 15.9068 4.75 14 4.75H10C8.09318 4.75 6.73851 4.75159 5.71085 4.88976ZM2.75 12C2.75 11.146 2.75032 10.4027 2.76309 9.75H21.2369C21.2497 10.4027 21.25 11.146 21.25 12V14C21.25 15.9068 21.2484 17.2615 21.1102 18.2892C20.975 19.2952 20.7213 19.8749 20.2981 20.2981C19.8749 20.7213 19.2952 20.975 18.2892 21.1102C17.2615 21.2484 15.9068 21.25 14 21.25H10C8.09318 21.25 6.73851 21.2484 5.71085 21.1102C4.70476 20.975 4.12511 20.7213 3.7019 20.2981C3.27869 19.8749 3.02502 19.2952 2.88976 18.2892C2.75159 17.2615 2.75 15.9068 2.75 14V12Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View 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

View 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

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 1.25C12.4142 1.25 12.75 1.58579 12.75 2V3.28169C16.9842 3.64113 20.3589 7.01581 20.7183 11.25H22C22.4142 11.25 22.75 11.5858 22.75 12C22.75 12.4142 22.4142 12.75 22 12.75H20.7183C20.3589 16.9842 16.9842 20.3589 12.75 20.7183V22C12.75 22.4142 12.4142 22.75 12 22.75C11.5858 22.75 11.25 22.4142 11.25 22V20.7183C7.01581 20.3589 3.64113 16.9842 3.28169 12.75H2C1.58579 12.75 1.25 12.4142 1.25 12C1.25 11.5858 1.58579 11.25 2 11.25H3.28169C3.64113 7.01581 7.01581 3.64113 11.25 3.28169V2C11.25 1.58579 11.5858 1.25 12 1.25ZM12 4.75C7.99594 4.75 4.75 7.99594 4.75 12C4.75 16.0041 7.99594 19.25 12 19.25C16.0041 19.25 19.25 16.0041 19.25 12C19.25 7.99594 16.0041 4.75 12 4.75ZM12 9.75C10.7574 9.75 9.75 10.7574 9.75 12C9.75 13.2426 10.7574 14.25 12 14.25C13.2426 14.25 14.25 13.2426 14.25 12C14.25 10.7574 13.2426 9.75 12 9.75ZM8.25 12C8.25 9.92893 9.92893 8.25 12 8.25C14.0711 8.25 15.75 9.92893 15.75 12C15.75 14.0711 14.0711 15.75 12 15.75C9.92893 15.75 8.25 14.0711 8.25 12Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

5
public/icons/logo.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg width="59" height="36" viewBox="0 0 59 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M58.417 17.7236L58.4238 17.7324L58.4316 17.7412C58.4817 17.8004 58.4921 17.8306 58.4941 17.8369C58.4964 17.8439 58.5 17.8568 58.5 17.9053C58.5 17.9544 58.4964 17.9668 58.4941 17.9736C58.4921 17.9799 58.4815 18.0094 58.4316 18.0684L58.4238 18.0771L58.417 18.0869L47.8486 31.8223L37.2822 18.0869L37.2744 18.0771L37.2676 18.0684L37.2178 18.001C37.2089 17.9857 37.2061 17.9767 37.2051 17.9736C37.2028 17.9668 37.1992 17.9545 37.1992 17.9053C37.1992 17.8567 37.2028 17.8439 37.2051 17.8369C37.2071 17.8307 37.2176 17.8004 37.2676 17.7412L37.2744 17.7324L37.2822 17.7236L47.8496 3.96973L58.417 17.7236Z" fill="#140B4E" stroke="#140B4E"/>
<path d="M21.7178 17.7236L21.7246 17.7324L21.7324 17.7412C21.7825 17.8004 21.7929 17.8306 21.7949 17.8369C21.7972 17.8439 21.8008 17.8568 21.8008 17.9053C21.8008 17.9544 21.7972 17.9668 21.7949 17.9736C21.7929 17.9799 21.7823 18.0094 21.7324 18.0684L21.7246 18.0771L21.7178 18.0869L11.1494 31.8223L0.583008 18.0869L0.575195 18.0771L0.568359 18.0684L0.518555 18.001C0.50972 17.9857 0.50687 17.9767 0.505859 17.9736C0.503628 17.9668 0.50001 17.9545 0.5 17.9053C0.5 17.8567 0.503619 17.8439 0.505859 17.8369C0.507889 17.8307 0.518333 17.8004 0.568359 17.7412L0.575195 17.7324L0.583008 17.7236L11.1504 3.96973L21.7178 17.7236Z" fill="#140B4E" stroke="#140B4E"/>
<path d="M43.5322 17.7197L43.5479 17.7373C43.6225 17.82 43.644 17.8685 43.6514 17.8896C43.6587 17.9108 43.6631 17.936 43.6631 18C43.6631 18.064 43.6587 18.0892 43.6514 18.1104C43.644 18.1315 43.6225 18.18 43.5479 18.2627L43.5322 18.2803L29.5449 35.3164L15.5586 18.2803L15.543 18.2627C15.4683 18.1799 15.4468 18.1315 15.4395 18.1104C15.4322 18.0893 15.4277 18.0641 15.4277 18C15.4277 17.9359 15.4322 17.9107 15.4395 17.8896C15.4468 17.8685 15.4683 17.8201 15.543 17.7373L15.5586 17.7197L29.5449 0.660156L43.5322 17.7197Z" fill="#EAE6F9" stroke="#140B4E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,21 @@
<svg width="42" height="66" viewBox="0 0 42 66" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M21 66C23.7614 66 26 64.8807 26 63.5C26 62.1193 23.7614 61 21 61C18.2386 61 16 62.1193 16 63.5C16 64.8807 18.2386 66 21 66Z" fill="#273343"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M21 0C32.598 0 42 9.40202 42 21C42 31.9234 33.6599 40.8989 23 41.906V62C23 63.1046 22.1046 64 21 64C19.8954 64 19 63.1046 19 62V41.906C8.34014 40.8989 0 31.9234 0 21C0 9.40202 9.40202 0 21 0Z" fill="white"/>
<foreignObject x="-61" y="-61" width="164" height="164"></foreignObject><g filter="url(#filter0_d_173_69)" data-figma-bg-blur-radius="65">
<path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z" fill="currentColor"/>
</g>
<path d="M28 21C28 17.134 24.866 14 21 14C17.134 14 14 17.134 14 21C14 24.866 17.134 28 21 28C24.866 28 28 24.866 28 21Z" fill="white"/>
<defs>
<filter id="filter0_d_173_69" x="-61" y="-61" width="164" height="164" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_173_69"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_173_69" result="shape"/>
</filter>
<clipPath id="bgblur_0_173_69_clip_path" transform="translate(61 61)"><path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z"/>
</clipPath></defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,20 @@
<svg width="30" height="35" viewBox="0 0 30 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4453 1.5127C24.4711 1.08082 31.4173 11.6767 27.2842 20.9209L27.0752 21.3672C25.9614 23.6466 24.4616 25.0302 23.0117 26.2031C21.6264 27.3238 20.4204 28.1673 19.3184 29.4482L19.0986 29.71C18.9038 29.9504 18.5282 30.5355 18.0273 31.2695C17.8006 31.6018 17.5566 31.95 17.332 32.2393C17.2202 32.3833 17.0974 32.5329 16.9727 32.666C16.8723 32.7731 16.6754 32.9748 16.4141 33.124L16.4131 33.123C15.7088 33.5252 14.941 33.6027 14.2256 33.3691C13.5676 33.1543 13.0766 32.7195 12.7383 32.3145V32.3135C12.3056 31.795 11.9245 31.2168 11.5986 30.7314C11.3403 30.3467 11.107 30.0062 10.8682 29.7012L10.626 29.4072C9.78615 28.4447 8.79047 27.6829 7.65723 26.7939C6.63272 25.9903 5.49745 25.0829 4.54102 23.9053L4.35254 23.666C-2.23923 15.0669 3.27657 2.37649 13.958 1.54199L14.4453 1.5127ZM14.4834 10.8291H14.4795C11.4118 11.1538 9.65384 14.8452 11.3496 17.6201L11.5254 17.8857L11.6885 18.1016C13.3725 20.1841 16.6502 20.1828 18.334 18.1006L18.4961 17.8857C20.762 14.6868 18.2266 10.4356 14.4834 10.8291Z" stroke="white" stroke-width="3"/>
<foreignObject x="-62" y="-62" width="154" height="159"></foreignObject><g filter="url(#filter0_d_225_89)" data-figma-bg-blur-radius="65">
<path d="M14.0746 3.0373C23.3145 2.3154 29.8796 12.2097 25.7277 20.7091C23.782 24.6909 20.5139 25.583 17.9337 28.7657C17.4406 29.3741 16.126 31.5614 15.6708 31.8213C14.974 32.2197 14.3495 31.903 13.8898 31.3525C13.1462 30.4615 12.5552 29.3355 11.756 28.4207C9.85145 26.2379 7.27127 25.0075 5.54262 22.7533C-0.347943 15.0689 4.61651 3.77623 14.0746 3.0373ZM14.325 9.33689C9.9238 9.79999 7.70872 15.095 10.3011 18.7534C12.5786 21.9678 17.444 21.9644 19.7203 18.7534C22.7123 14.5298 19.3874 8.80455 14.3261 9.33689H14.325Z" fill="currentColor"/>
</g>
<defs>
<filter id="filter0_d_225_89" x="-62" y="-62" width="154" height="159" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_225_89"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_225_89" result="shape"/>
</filter>
<clipPath id="bgblur_0_225_89_clip_path" transform="translate(62 62)"><path d="M14.0746 3.0373C23.3145 2.3154 29.8796 12.2097 25.7277 20.7091C23.782 24.6909 20.5139 25.583 17.9337 28.7657C17.4406 29.3741 16.126 31.5614 15.6708 31.8213C14.974 32.2197 14.3495 31.903 13.8898 31.3525C13.1462 30.4615 12.5552 29.3355 11.756 28.4207C9.85145 26.2379 7.27127 25.0075 5.54262 22.7533C-0.347943 15.0689 4.61651 3.77623 14.0746 3.0373ZM14.325 9.33689C9.9238 9.79999 7.70872 15.095 10.3011 18.7534C12.5786 21.9678 17.444 21.9644 19.7203 18.7534C22.7123 14.5298 19.3874 8.80455 14.3261 9.33689H14.325Z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View 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.95153 1.04102H10.0472C11.9708 1.04101 13.4784 1.041 14.6546 1.19914C15.8585 1.361 16.8086 1.69879 17.5542 2.44446C18.2999 3.19014 18.6377 4.14016 18.7996 5.3441C18.9577 6.52034 18.9577 8.02788 18.9577 9.95154V10.0472C18.9577 11.9708 18.9577 13.4784 18.7996 14.6546C18.6377 15.8585 18.2999 16.8086 17.5542 17.5542C16.8086 18.2999 15.8585 18.6377 14.6546 18.7996C13.4784 18.9577 11.9708 18.9577 10.0472 18.9577H9.95154C8.02788 18.9577 6.52034 18.9577 5.3441 18.7996C4.14016 18.6377 3.19014 18.2999 2.44446 17.5542C1.69879 16.8086 1.361 15.8585 1.19914 14.6546C1.041 13.4784 1.04101 11.9708 1.04102 10.0472V9.95153C1.04101 8.02787 1.041 6.52034 1.19914 5.3441C1.361 4.14016 1.69879 3.19014 2.44446 2.44446C3.19014 1.69879 4.14016 1.361 5.3441 1.19914C6.52034 1.041 8.02787 1.04101 9.95153 1.04102ZM5.51066 2.43799C4.44533 2.58122 3.80306 2.85363 3.32835 3.32835C2.85363 3.80306 2.58122 4.44533 2.43799 5.51066C2.29234 6.59398 2.29102 8.01749 2.29102 9.99935C2.29102 11.9812 2.29234 13.4047 2.43799 14.488C2.58122 15.5534 2.85363 16.1956 3.32835 16.6704C3.80306 17.1451 4.44533 17.4175 5.51066 17.5607C6.59398 17.7064 8.01749 17.7077 9.99935 17.7077C11.9812 17.7077 13.4047 17.7064 14.488 17.5607C15.5534 17.4175 16.1956 17.1451 16.6704 16.6704C17.1451 16.1956 17.4175 15.5534 17.5607 14.488C17.7064 13.4047 17.7077 11.9812 17.7077 9.99935C17.7077 8.01749 17.7064 6.59398 17.5607 5.51066C17.4175 4.44533 17.1451 3.80306 16.6704 3.32835C16.1956 2.85363 15.5534 2.58122 14.488 2.43799C13.4047 2.29234 11.9812 2.29102 9.99935 2.29102C8.01749 2.29102 6.59398 2.29234 5.51066 2.43799ZM8.76372 5.3801C9.01367 5.61815 9.02332 6.01376 8.78527 6.26372L6.40432 8.76372C6.28635 8.88758 6.12278 8.95768 5.95173 8.95768C5.78068 8.95768 5.61711 8.88758 5.49914 8.76372L4.54676 7.76372C4.30871 7.51376 4.31836 7.11815 4.56831 6.8801C4.81827 6.64204 5.21388 6.65169 5.45194 6.90165L5.95173 7.42643L7.8801 5.40165C8.11815 5.15169 8.51376 5.14204 8.76372 5.3801ZM10.2077 7.49935C10.2077 7.15417 10.4875 6.87435 10.8327 6.87435H14.9993C15.3445 6.87435 15.6243 7.15417 15.6243 7.49935C15.6243 7.84453 15.3445 8.12435 14.9993 8.12435H10.8327C10.4875 8.12435 10.2077 7.84453 10.2077 7.49935ZM8.76372 11.2134C9.01367 11.4515 9.02332 11.8471 8.78527 12.097L6.40432 14.5971C6.28635 14.7209 6.12278 14.791 5.95173 14.791C5.78068 14.791 5.61711 14.7209 5.49914 14.5971L4.54676 13.5971C4.30871 13.3471 4.31836 12.9515 4.56831 12.7134C4.81827 12.4754 5.21388 12.485 5.45194 12.735L5.95173 13.2598L7.8801 11.235C8.11815 10.985 8.51376 10.9754 8.76372 11.2134ZM10.2077 13.3327C10.2077 12.9875 10.4875 12.7077 10.8327 12.7077H14.9993C15.3445 12.7077 15.6243 12.9875 15.6243 13.3327C15.6243 13.6779 15.3445 13.9577 14.9993 13.9577H10.8327C10.4875 13.9577 10.2077 13.6779 10.2077 13.3327Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

12
public/icons/qrcode.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.0 KiB

3
public/icons/search.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 2.75C6.66751 2.75 2.75 6.66751 2.75 11.5C2.75 16.3325 6.66751 20.25 11.5 20.25C16.3325 20.25 20.25 16.3325 20.25 11.5C20.25 6.66751 16.3325 2.75 11.5 2.75ZM1.25 11.5C1.25 5.83908 5.83908 1.25 11.5 1.25C17.1609 1.25 21.75 5.83908 21.75 11.5C21.75 14.0605 20.8111 16.4017 19.2589 18.1982L22.5303 21.4697C22.8232 21.7626 22.8232 22.2374 22.5303 22.5303C22.2374 22.8232 21.7626 22.8232 21.4697 22.5303L18.1982 19.2589C16.4017 20.8111 14.0605 21.75 11.5 21.75C5.83908 21.75 1.25 17.1609 1.25 11.5Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

11
src/app/(main)/layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import BottomSheet from "@/components/UI/BottomSheet";
import MapProviderComponent from "@/providers/MapProviderComponent";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<MapProviderComponent>
{children}
<BottomSheet />
</MapProviderComponent>
);
}

View File

@@ -11,6 +11,7 @@ import ToastProvider from "@/utils/ToastProvider";
import { Sidebar } from "@/components/SideBar";
import { AppShell } from "@/providers/AppShell";
import ServiceWorkerRegister from "@/components/ServiceWorkerRegister";
import DeviceProvider from "@/providers/DeviceProvider";
export const metadata: Metadata = {
title: {
@@ -50,19 +51,25 @@ export default async function RootLayout({ children }: { children: React.ReactNo
const messages = await getMessages({ locale });
return (
<html lang={locale} dir="rtl">
<body>
<body
style={{
backgroundColor: "var(--color-map-bg)",
}}
>
<ServiceWorkerRegister />
<NextTopLoader color="#16a795" />
<NextIntlClientProvider locale={locale} messages={messages}>
<ClientAppProvider>
<AppShell>
<UserInitializer />
{children}
<Modal />
<ToastProvider />
<Sidebar />
</AppShell>
</ClientAppProvider>
<DeviceProvider>
<ClientAppProvider>
<AppShell>
<UserInitializer />
{children}
<Modal />
<ToastProvider />
<Sidebar />
</AppShell>
</ClientAppProvider>
</DeviceProvider>
</NextIntlClientProvider>
</body>
</html>

View File

@@ -6,3 +6,19 @@ export { default as LogoutIcon } from "&/icons/logout.svg";
export { default as AvatarIcon } from "&/icons/avatar-icon.svg";
export { default as DarkModeIcon } from "&/icons/dark-mode-icon.svg";
export { default as LightModeIcon } from "&/icons/light-mode-icon.svg";
export { default as LogoIcon } from "&/icons/logo.svg";
export { default as MyOrderIcon } from "&/icons/my-order.svg";
export { default as BannerImage } from "&/images/homePageBannerImage.png";
export { default as BannerStar } from "&/icons/banner-star.svg";
export { default as BannerLine } from "&/icons/banner-line.svg";
export { default as ArrowTopLeft } from "&/icons/arrow-tl.svg";
export { default as SearchIcon } from "&/icons/search.svg";
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";

View File

@@ -0,0 +1,79 @@
import { MarkerIcon } from "@/assets";
import { LatLng } from "@/stores/LocationStore";
import { Marker, MarkerDragEvent } from "@vis.gl/react-maplibre";
import { AnimatePresence, motion } from "framer-motion";
import { useState } from "react";
type Props = {
location: LatLng;
onDragEnd: (e: MarkerDragEvent) => void;
};
const dropInVariants = {
initial: { y: -50, opacity: 0, scale: 0.8 },
animate: {
y: 0,
opacity: 1,
scale: 1,
transition: {
type: "spring" as const,
stiffness: 600,
damping: 30,
},
},
};
export default function LocationMarker({ location, onDragEnd }: Props) {
const [isDragging, setIsDragging] = useState(false);
const handleDragStart = () => {
setIsDragging(true);
};
const handleDragEnd = (e: MarkerDragEvent) => {
setIsDragging(false);
onDragEnd(e);
};
return (
<Marker
latitude={location.lat}
longitude={location.lng}
draggable
anchor="bottom"
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<AnimatePresence mode="wait">
<motion.div
variants={dropInVariants}
initial="initial"
animate={{
y: isDragging ? -20 : 0,
opacity: 1,
transition: {
type: "spring" as const,
stiffness: 500,
damping: 25,
},
}}
exit="exit"
className="relative flex flex-col items-center"
>
{/* Drop pointer inside the marker (shown only when dragging) */}
{isDragging && (
<motion.div
layoutId="drop-pointer"
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 0.5, y: 10 }}
exit={{ scale: 0, opacity: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className="absolute -bottom-4.5 z-[-1] size-1.5 rounded-full bg-black shadow-md"
/>
)}
<MarkerIcon className="text-neo-aqua size-8" />
</motion.div>
</AnimatePresence>
</Marker>
);
}

View File

@@ -0,0 +1,62 @@
"use client";
import MainMap from "@/components/MainMap";
import { motion, AnimatePresence } from "framer-motion";
import LocationMarker from "./LocationMarker";
import { useModalStore } from "@/stores/useModalStore";
import { CloseIcon } from "@/assets";
import MarkerPosition from "@/components/Home/MarkerPosition";
const backdropVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
exit: { opacity: 0 },
};
const modalVariants = {
hidden: { opacity: 0, scale: 0.9 },
visible: {
opacity: 1,
scale: 1,
transition: { stiffness: 300, damping: 25 },
},
exit: { opacity: 0, scale: 0.9 },
};
const LocationModal = () => {
const closeModal = useModalStore((s) => s.closeModal);
return (
<AnimatePresence>
<motion.div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
variants={backdropVariants}
initial="hidden"
animate="visible"
exit="exit"
onClick={closeModal}
>
<motion.div
className="relative h-[50vh] w-[90vw] rounded-lg shadow-lg sm:h-[40vh] sm:w-[40vw] lg:h-[60vh] lg:w-[50vw]"
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={closeModal}
className="absolute top-2 left-2 z-50 cursor-pointer rounded-full bg-black/60 p-2 transition hover:bg-black/60"
>
<CloseIcon className="h-5 w-5 text-white" />
</button>
<div className="relative h-full w-full">
<MainMap>
<MarkerPosition onChange={(loc) => console.log(loc)} />
</MainMap>
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
};
export default LocationModal;

View File

@@ -0,0 +1,53 @@
import SearchItem from "./SearchItem";
import { CalenderIcon, LocationIcon, SearchIcon } from "@/assets";
import { useModalStore } from "@/stores/useModalStore";
import LocationModal from "./LocationModal";
import NoFormSelect from "@/components/UI/NoFormSelect";
import DatePickerField from "@/components/UI/DatePickerField";
const options = [
{ label: "Home", value: "home" },
{ label: "Search", value: "search" },
];
export default function SearchBar() {
const openModal = useModalStore((s) => s.openModal);
return (
<div className="bg-card flex max-w-2xl items-center justify-between gap-1 rounded-full p-4">
{/* آدرس */}
<SearchItem icon={<LocationIcon className="size-5" />}>
<button
type="button"
onClick={() => openModal(<LocationModal />)}
className="w-full text-right text-text-primary"
>
آدرس
</button>
</SearchItem>
{/* تاریخ */}
<SearchItem icon={<CalenderIcon className="size-5" />}>
<DatePickerField
name="bill_of_lading_date"
setValue={() => {
console.log("hello DatePickerField");
}}
/>
</SearchItem>
{/* دسته بندی */}
<SearchItem icon={<SearchIcon className="size-5" />}>
<NoFormSelect
value={""}
onChange={() => {
console.log("hello");
}}
options={options}
loading={false}
error={null}
placeholder="علت انسداد"
// onOpen={fetchReasons}
/>
</SearchItem>
</div>
);
}

View File

@@ -0,0 +1,15 @@
interface SearchItemProps {
icon: React.ReactNode;
children: React.ReactNode;
}
function SearchItem({ icon, children }: SearchItemProps) {
return (
<div className="flex min-w-[180px] items-center gap-2 rounded-full bg-white px-4 py-2 shadow-sm">
<div className="text-black">{icon}</div>
<div className="flex-1">{children}</div>
</div>
);
}
export default SearchItem;

View File

@@ -0,0 +1,177 @@
"use client";
import { ArrowTopLeft, AvatarIcon, BannerImage, BannerLine, BannerStar, LogoIcon, MyOrderIcon } from "@/assets";
import { useTranslations } from "next-intl";
import userStore from "@/stores/userStore";
import Image from "next/image";
import ThemeToggle from "@/components/ThemeToggle";
import { motion } from "framer-motion";
import GroupAvatar from "@/components/UI/GroupAvatar";
import SearchBar from "@/components/Home/Desktop/Banner/SearchBar";
export default function BannerComponent() {
const t = useTranslations("HomePage");
const isAuth = userStore((s) => s.isAuth);
return (
<motion.div
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 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 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-8 lg:size-10" />
</motion.div>
<ThemeToggle />
{["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 text-center hover:text-primary-200 cursor-pointer transition text-xs lg:text-sm"
>
{t(item)}
</motion.p>
))}
</div>
<div className="flex w-full items-center justify-center">
{isAuth ? (
<motion.div whileHover={{ scale: 1.1 }} className="cursor-pointer">
<AvatarIcon className="size-4 lg:size-5" />
</motion.div>
) : (
<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="size-5 lg:size-6" />
<p className="text-xs lg:text-base">{t("my_order")}</p>
</motion.button>
<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"
>
<AvatarIcon className="size-4 lg:size-5" />
<p className="text-xs lg:text-base">{t("auth")}</p>
</motion.button>
</span>
)}
</div>
</header>
{/* ================= Main ================= */}
<motion.main initial="hidden" animate="visible" className="flex w-full items-center justify-center py-12">
{/* ===== Left Content ===== */}
<div className="flex w-full flex-col items-center justify-center space-y-7">
{/* Title */}
<motion.div className="text-2xl lg:text-4xl font-bold">
<motion.span
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}}
className="block"
>
{t("main_title_line1")}
</motion.span>
<motion.div
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}}
className="flex items-center justify-between"
>
{t("main_title_line2")}
<motion.div
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ delay: 0.6, duration: 0.4 }}
className="origin-right"
>
<BannerLine className="h-1 w-28" />
</motion.div>
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ repeat: Infinity, duration: 3 }}
>
<BannerStar className="h-6 w-6" />
</motion.div>
</motion.div>
</motion.div>
{/* Description */}
<motion.p
variants={{
hidden: { opacity: 0 },
visible: { opacity: 1 },
}}
className="text-center px-10 text-xs"
>
{t("des")}
</motion.p>
{/* CTA Button */}
<motion.button
whileHover={{ scale: 1.07 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 300 }}
className="bg-primary-200 flex items-center justify-center gap-x-3 rounded-lg p-3"
>
<p className="text-white">{t("see_salons")}</p>
<motion.div whileHover={{ x: -4, y: -4 }}>
<ArrowTopLeft className="size-5 text-white" />
</motion.div>
</motion.button>
<GroupAvatar />
</div>
{/* ===== Right Image ===== */}
<motion.div
// 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"
className="object-cover"
width={400}
height={200}
priority
/>
</motion.div>
</motion.div>
</motion.main>
<div className="absolute -bottom-10 left-10 lg:left-24">
<SearchBar />
</div>
</motion.div>
);
}

View 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>
);
}

View File

@@ -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;

View 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>
);
}

View 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>
);
}

View 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;

View 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>
);
}

View File

@@ -1,3 +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 <></>;
return (
<div className="container mx-auto h-full w-full">
<BannerComponent />
<Categories />
<Reservation />
<Salons />
<App />
</div>
);
}

View File

@@ -0,0 +1,65 @@
"use client";
import { MarkerIcon, MarkerPreviewIcon } from "@/assets";
import { useMap } from "@vis.gl/react-maplibre";
import maplibregl from "maplibre-gl";
import { useEffect, useRef } from "react";
import { renderToStaticMarkup } from "react-dom/server";
const createMarkerElement = (preview: boolean) => {
const el = document.createElement("div");
const Icon = preview ? MarkerPreviewIcon : MarkerIcon;
el.innerHTML = renderToStaticMarkup(<Icon className="text-primary-100 size-10" />);
return el;
};
type Props = {
preview?: boolean;
onChange?: (location: { lat: number; lng: number }) => void;
};
const MarkerPosition = ({ preview = false, onChange }: Props) => {
const { current: homePageMap } = useMap();
const markerRef = useRef<maplibregl.Marker | null>(null);
useEffect(() => {
if (!homePageMap) return;
const map = homePageMap.getMap();
if (!map) return;
// create marker once
if (!markerRef.current) {
const el = createMarkerElement(preview);
markerRef.current = new maplibregl.Marker({
element: el,
anchor: "bottom",
subpixelPositioning: true,
})
.setLngLat(map.getCenter())
.addTo(map);
}
const onMove = () => {
const center = map.getCenter();
markerRef.current?.setLngLat(center);
onChange?.({
lat: center.lat,
lng: center.lng,
});
};
map.on("move", onMove);
return () => {
map.off("move", onMove);
markerRef.current?.remove();
markerRef.current = null;
};
}, [homePageMap, preview]);
return null;
};
export default MarkerPosition;

View File

@@ -0,0 +1,79 @@
import { MarkerIcon } from "@/assets";
import { LatLng } from "@/stores/LocationStore";
import { Marker, MarkerDragEvent } from "@vis.gl/react-maplibre";
import { AnimatePresence, motion } from "framer-motion";
import { useState } from "react";
type Props = {
location: LatLng;
onDragEnd: (e: MarkerDragEvent) => void;
};
const dropInVariants = {
initial: { y: -50, opacity: 0, scale: 0.8 },
animate: {
y: 0,
opacity: 1,
scale: 1,
transition: {
type: "spring" as const,
stiffness: 600,
damping: 30,
},
},
};
export default function LocationMarker({ location, onDragEnd }: Props) {
const [isDragging, setIsDragging] = useState(false);
const handleDragStart = () => {
setIsDragging(true);
};
const handleDragEnd = (e: MarkerDragEvent) => {
setIsDragging(false);
onDragEnd(e);
};
return (
<Marker
latitude={location.lat}
longitude={location.lng}
draggable
anchor="bottom"
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<AnimatePresence mode="wait">
<motion.div
variants={dropInVariants}
initial="initial"
animate={{
y: isDragging ? -20 : 0,
opacity: 1,
transition: {
type: "spring" as const,
stiffness: 500,
damping: 25,
},
}}
exit="exit"
className="relative flex flex-col items-center"
>
{/* Drop pointer inside the marker (shown only when dragging) */}
{isDragging && (
<motion.div
layoutId="drop-pointer"
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 0.5, y: 10 }}
exit={{ scale: 0, opacity: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className="absolute -bottom-4.5 z-[-1] size-1.5 rounded-full bg-black shadow-md"
/>
)}
<MarkerIcon className="text-neo-aqua size-8" />
</motion.div>
</AnimatePresence>
</Marker>
);
}

View File

@@ -0,0 +1,62 @@
"use client";
import MainMap from "@/components/MainMap";
import { motion, AnimatePresence } from "framer-motion";
import LocationMarker from "./LocationMarker";
import { useModalStore } from "@/stores/useModalStore";
import { CloseIcon } from "@/assets";
import MarkerPosition from "@/components/Home/MarkerPosition";
const backdropVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
exit: { opacity: 0 },
};
const modalVariants = {
hidden: { opacity: 0, scale: 0.9 },
visible: {
opacity: 1,
scale: 1,
transition: { stiffness: 300, damping: 25 },
},
exit: { opacity: 0, scale: 0.9 },
};
const LocationModal = () => {
const closeModal = useModalStore((s) => s.closeModal);
return (
<AnimatePresence>
<motion.div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
variants={backdropVariants}
initial="hidden"
animate="visible"
exit="exit"
onClick={closeModal}
>
<motion.div
className="relative h-[50vh] w-[90vw] rounded-lg shadow-lg sm:h-[40vh] sm:w-[40vw] lg:h-[60vh] lg:w-[50vw]"
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={closeModal}
className="absolute top-2 left-2 z-50 cursor-pointer rounded-full bg-black/60 p-2 transition hover:bg-black/60"
>
<CloseIcon className="h-5 w-5 text-white" />
</button>
<div className="relative h-full w-full">
<MainMap>
<MarkerPosition onChange={(loc) => console.log(loc)} />
</MainMap>
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
};
export default LocationModal;

View File

@@ -0,0 +1,53 @@
import SearchItem from "./SearchItem";
import { CalenderIcon, LocationIcon, SearchIcon } from "@/assets";
import { useModalStore } from "@/stores/useModalStore";
import LocationModal from "./LocationModal";
import NoFormSelect from "@/components/UI/NoFormSelect";
import DatePickerField from "@/components/UI/DatePickerField";
const options = [
{ label: "Home", value: "home" },
{ label: "Search", value: "search" },
];
export default function SearchBar() {
const openModal = useModalStore((s) => s.openModal);
return (
<div className="bg-card flex max-w-2xl items-center justify-between gap-1 rounded-full p-4">
{/* آدرس */}
<SearchItem icon={<LocationIcon className="size-5" />}>
<button
type="button"
onClick={() => openModal(<LocationModal />)}
className="w-full text-right text-gray-500"
>
آدرس
</button>
</SearchItem>
{/* تاریخ */}
<SearchItem icon={<CalenderIcon className="size-5" />}>
<DatePickerField
name="bill_of_lading_date"
setValue={() => {
console.log("hello DatePickerField");
}}
/>
</SearchItem>
{/* دسته بندی */}
<SearchItem icon={<SearchIcon className="size-5" />}>
<NoFormSelect
value={""}
onChange={() => {
console.log("hello");
}}
options={options}
loading={false}
error={null}
placeholder="علت انسداد"
// onOpen={fetchReasons}
/>
</SearchItem>
</div>
);
}

View File

@@ -0,0 +1,15 @@
interface SearchItemProps {
icon: React.ReactNode;
children: React.ReactNode;
}
function SearchItem({ icon, children }: SearchItemProps) {
return (
<div className="flex min-w-[180px] items-center gap-2 rounded-full bg-white px-4 py-2 shadow-sm">
<div className="text-black">{icon}</div>
<div className="flex-1">{children}</div>
</div>
);
}
export default SearchItem;

View File

@@ -0,0 +1,169 @@
"use client";
import { ArrowTopLeft, AvatarIcon, BannerImage, BannerLine, BannerStar, LogoIcon, MyOrderIcon } from "@/assets";
import { useTranslations } from "next-intl";
import userStore from "@/stores/userStore";
import Image from "next/image";
import ThemeToggle from "@/components/ThemeToggle";
import { motion } from "framer-motion";
import GroupAvatar from "@/components/UI/GroupAvatar";
import SearchBar from "@/components/Home/Desktop/Banner/SearchBar";
export default function BannerComponent() {
const t = useTranslations("HomePage");
const isAuth = userStore((s) => s.isAuth);
return (
<motion.div
initial={{ opacity: 0, y: 60 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, ease: "easeOut" }}
className="bg-home-page-banner relative mt-16 rounded-tr-[200px] rounded-bl-[200px] 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">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 200 }}
>
<LogoIcon className="size-12" />
</motion.div>
<ThemeToggle />
{["home", "search", "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"
>
{t(item)}
</motion.p>
))}
</div>
<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" />
</motion.div>
) : (
<span className="flex w-full items-center justify-center 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>
</motion.button>
<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"
>
<AvatarIcon className="h-6 w-6" />
<p>{t("auth")}</p>
</motion.button>
</span>
)}
</div>
</header>
{/* ================= Main ================= */}
<motion.main initial="hidden" animate="visible" className="flex w-full items-center justify-center py-12">
{/* ===== 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.span
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}}
className="block"
>
{t("main_title_line1")}
</motion.span>
<motion.div
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}}
className="flex items-center justify-between"
>
{t("main_title_line2")}
<motion.div
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ delay: 0.6, duration: 0.4 }}
className="origin-right"
>
<BannerLine className="h-1 w-28" />
</motion.div>
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ repeat: Infinity, duration: 3 }}
>
<BannerStar className="h-6 w-6" />
</motion.div>
</motion.div>
</motion.div>
{/* Description */}
<motion.p
variants={{
hidden: { opacity: 0 },
visible: { opacity: 1 },
}}
className="text-center text-xs"
>
{t("des")}
</motion.p>
{/* CTA Button */}
<motion.button
whileHover={{ scale: 1.07 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 300 }}
className="bg-primary-200 flex items-center justify-center gap-x-3 rounded-lg p-3"
>
<p className="text-white">{t("see_salons")}</p>
<motion.div whileHover={{ x: -4, y: -4 }}>
<ArrowTopLeft className="size-5 text-white" />
</motion.div>
</motion.button>
<GroupAvatar />
</div>
{/* ===== Right Image ===== */}
<motion.div
animate={{ y: [0, -12, 0] }}
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
className="flex w-full items-center justify-center"
>
<Image
src={BannerImage}
alt="main_image"
className="object-cover"
width={400}
height={200}
priority
/>
</motion.div>
</motion.main>
<div className="absolute -bottom-10 left-24">
<SearchBar />
</div>
</motion.div>
);
}

View File

@@ -1,3 +1,3 @@
export default function MobileHome() {
return <></>;
return <div className="h-full w-full"></div>;
}

View File

@@ -0,0 +1,113 @@
"use client";
import { Map, MapLayerMouseEvent } from "@vis.gl/react-maplibre";
import { ReactNode, useMemo } from "react";
import maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { useMapMove } from "@/utils/useMapMove";
import { useMapLayersStore } from "@/stores/useMapLayersStore";
import { useMapStore } from "@/stores/useMapStore";
// Read From Environment
const BRIGHT_MAP_TILE = process.env.NEXT_PUBLIC_MAP_BRIGHT!;
const DARK_MAP_TILE = process.env.NEXT_PUBLIC_MAP_DARK!;
const STORAGE_KEY = process.env.NEXT_PUBLIC_MAP_VIEW_LOCAL_STORAGE_KEY!;
const MIN_ZOOM = parseFloat(process.env.NEXT_PUBLIC_MAP_MIN_ZOOM!);
const MAX_ZOOM = parseFloat(process.env.NEXT_PUBLIC_MAP_MAX_ZOOM!);
type MainMapProps = {
id?: string;
mapLayer?: string;
children?: ReactNode;
initialBound?: [number, number];
initialZoom?: number;
ref?: React.Ref<any>;
interactiveLayerIds?: string[];
handleMapClick?: (e: maplibregl.MapLayerMouseEvent) => void;
handleContextMenu?: (e: maplibregl.MapMouseEvent & maplibregl.Event) => void;
handleMapInteraction?: () => void;
onMouseMove?: (e: MapLayerMouseEvent) => void;
onMouseLeave?: () => void;
cursor?: string | null;
};
type MapView = {
longitude: number;
latitude: number;
zoom: number;
};
export default function MainMap({
id,
ref,
onMouseLeave,
onMouseMove,
children,
initialBound = [34.5468992, 52.7300532],
initialZoom = 5.5,
handleMapClick,
interactiveLayerIds,
mapLayer,
handleContextMenu,
handleMapInteraction,
cursor,
}: MainMapProps) {
const { mapMove } = useMapMove();
const activeLayer = useMapLayersStore((s) => s.activeLayer);
const setIsMapMoving = useMapStore((s) => s.setIsMapMoving);
const parsed = useMemo(() => {
if (typeof window === "undefined") return null;
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? (JSON.parse(saved) as MapView) : null;
}, []);
const MAP_STYLES = useMemo(
() => ({
dark: DARK_MAP_TILE,
bright: BRIGHT_MAP_TILE,
}),
[]
);
return (
<div className="relative h-full w-full">
<Map
ref={ref}
id={id ? id : "homePageMap"}
interactiveLayerIds={interactiveLayerIds}
initialViewState={{
longitude: parsed ? parsed.longitude : initialBound[1],
latitude: parsed ? parsed.latitude : initialBound[0],
zoom: parsed ? parsed.zoom : initialZoom,
}}
onError={(e) => {
console.error("Map error:", e);
}}
maxBounds={[
[35.0, 18.0], // west, south
[74.5, 44.0], // east, north
]}
onMoveStart={() => setIsMapMoving(true)}
RTLTextPlugin={"/mapbox-gl-rtl-text.js"}
onDragStart={handleMapInteraction}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
onZoomStart={handleMapInteraction}
onMoveEnd={(e) => {
setIsMapMoving(false);
mapMove(e);
}}
onClick={handleMapClick}
onContextMenu={handleContextMenu}
cursor={cursor === "default" ? undefined : "auto"}
mapStyle={mapLayer ? mapLayer : MAP_STYLES[activeLayer]}
minZoom={MIN_ZOOM}
maxZoom={MAX_ZOOM}
attributionControl={false}
style={{ width: "100%", height: "100%" }}
>
{children}
</Map>
</div>
);
}

View 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 };

View File

@@ -0,0 +1,74 @@
"use client";
import { CalendarHijri } from "@/components/UI/Calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/UI/Popover";
import { motion } from "framer-motion";
import { useState } from "react";
import { Path, UseFormSetValue } from "react-hook-form";
type Props<T extends Record<string, any>> = {
name: Path<T>;
label?: string | null;
setValue: UseFormSetValue<T>;
defaultValue?: string | null;
};
export default function DatePickerField<T extends Record<string, any>>({
name,
label,
setValue,
defaultValue,
}: Props<T>) {
const [date, setDate] = useState<Date | undefined>(defaultValue ? new Date(defaultValue) : undefined);
const [open, setOpen] = useState(false);
// format date into Persian numerals
const formatJalali = (date?: Date) => {
if (!date) return "";
const formatter = new Intl.DateTimeFormat("fa-IR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
return formatter.format(date);
};
return (
<div className="relative flex flex-col justify-between gap-1">
<motion.label
className="text-sm font-medium"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
>
{label}
</motion.label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
onClick={() => setOpen(true)}
className={"rounded-lg px-3 text-start text-black focus:outline-none"}
>
{date ? formatJalali(date) : "تاریخ"}
</button>
</PopoverTrigger>
<PopoverContent align="center" className="z-50 w-auto p-0" sideOffset={8}>
<div>
<CalendarHijri
selected={date}
onSelect={(selectedDate) => {
setDate(selectedDate);
if (selectedDate) {
setValue(name, selectedDate.toISOString() as any, { shouldValidate: true });
setOpen(false);
}
}}
/>
</div>
</PopoverContent>
</Popover>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/UI/avatar";
export default function GroupAvatar() {
return (
<div className="bg-card flex -space-x-[0.6rem] rounded-2xl rounded-bl-none p-3">
<Avatar className="ring-primary-200 ring-1">
<AvatarImage
alt="U1"
src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>U1</AvatarFallback>
</Avatar>
<Avatar className="ring-primary-200 ring-1">
<AvatarImage
alt="U2"
src="https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>U2</AvatarFallback>
</Avatar>
<Avatar className="ring-primary-200 ring-1">
<AvatarImage
alt="U3"
src="https://images.unsplash.com/photo-1655874819398-c6dfbec68ac7?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>U3</AvatarFallback>
</Avatar>
<Avatar className="ring-primary-200 ring-1">
<AvatarImage
alt="U3"
src="https://images.unsplash.com/photo-1655874819398-c6dfbec68ac7?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>U3</AvatarFallback>
</Avatar>
</div>
);
}

View File

@@ -0,0 +1,88 @@
"use client";
import React from "react";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/UI/Select";
import { motion } from "framer-motion";
interface Option {
value: string | number;
label: string;
}
interface ReusableSelectProps {
value: string | number;
onChange: (value: string | number) => void;
options: Option[];
loading?: boolean;
error?: string | null;
placeholder?: string;
onOpen?: () => void;
}
const NoFormSelect: React.FC<ReusableSelectProps> = ({
value,
onChange,
options,
loading = false,
error = null,
placeholder = "انتخاب کنید",
onOpen,
}) => {
return (
<Select
value={value?.toString() || ""}
onValueChange={(val) => onChange(val)}
onOpenChange={(open) => {
if (open && onOpen) onOpen();
}}
>
<SelectTrigger className={`"text-black w-full overflow-x-scroll rounded-lg placeholder-black`}>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent className="bg-card overflow-y-auto rounded-xl border-0 shadow-lg">
{loading && (
<SelectItem value="loading" disabled>
در حال بارگذاری...
</SelectItem>
)}
{error && (
<SelectItem value="error" disabled>
{error}
</SelectItem>
)}
{!loading && !error && (
<>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="rounded px-2 py-1"
>
<SelectItem
value="all"
className="hover:from-primary-400 hover:to-primary-500 cursor-pointer hover:bg-linear-to-l"
>
تمامی موارد
</SelectItem>
</motion.div>
{options.map((opt) => (
<motion.div
key={opt.value}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="rounded px-2 py-1"
>
<SelectItem
value={opt.value.toString()}
className="hover:from-primary-400 hover:to-primary-500 cursor-pointer hover:bg-linear-to-l"
>
{opt.label}
</SelectItem>
</motion.div>
))}
</>
)}
</SelectContent>
</Select>
);
};
export default NoFormSelect;

View File

@@ -26,7 +26,7 @@ function PopoverContent({
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
"bg-card text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md p-4 shadow-md outline-hidden",
className
)}
{...props}

View File

@@ -25,14 +25,14 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground 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:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md bg-transparent px-3 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-black data-[size=default]:h-7 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-black",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronIcon className="text-text size-4 rotate-270" />
<ChevronIcon className="text-text size-3 rotate-270" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
@@ -48,7 +48,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className

View File

@@ -0,0 +1,40 @@
"use client";
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
import { cn } from "@/lib/utils";
function Avatar({ className, ...props }: AvatarPrimitive.Root.Props) {
return (
<AvatarPrimitive.Root
className={cn(
"bg-background inline-flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-full align-middle text-xs font-medium select-none",
className
)}
data-slot="avatar"
{...props}
/>
);
}
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
return (
<AvatarPrimitive.Image
className={cn("size-full object-cover", className)}
data-slot="avatar-image"
{...props}
/>
);
}
function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props) {
return (
<AvatarPrimitive.Fallback
className={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
data-slot="avatar-fallback"
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback };

View 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 };

View 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
View 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",
},
];

View File

@@ -0,0 +1,11 @@
import { create } from "zustand";
type MapStoreState = {
isMapMoving: boolean;
setIsMapMoving: (isMoving: boolean) => void;
};
export const useMapStore = create<MapStoreState>((set) => ({
isMapMoving: false,
setIsMapMoving: (isMapMoving) => set({ isMapMoving }),
}));

View File

@@ -131,6 +131,7 @@ body {
/* =====================
Primary (Purple)
====================== */
--color-primary-50: #140b4e;
--color-primary-100: #7d34b9;
--color-primary-200: #9a5cff;
--color-primary-300: #b38bff;
@@ -152,6 +153,11 @@ body {
--color-text-secondary: #6b7280;
--color-text-muted: #9ca3af;
/* =====================
home page banner color
=========================*/
--color-home-page-banner: #ede9ff;
/* =====================
State colors
====================== */
@@ -186,9 +192,15 @@ body {
--color-text-muted: #9ca3af;
--color-primary-100: #b38bff;
--color-primary-50: #140b4e;
--color-primary-200: #9a5cff;
--color-primary-300: #7d34b9;
/* =====================
home page banner color
=========================*/
--color-home-page-banner: #1d1c1d;
--color-gray-50: #1f1f1f;
--color-gray-100: #262626;
--color-gray-200: #2a2a2a;