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