🚀 Complete Laca City Website with VPS Deployment

- Added complete Next.js frontend with responsive design
- Added NestJS backend with PostgreSQL and Redis
- Added comprehensive VPS deployment script (vps-deploy.sh)
- Added deployment guide and documentation
- Added all assets and static files
- Configured SSL, Nginx, PM2, and monitoring
- Ready for production deployment on any VPS
This commit is contained in:
2025-08-12 07:06:15 +07:00
parent bc87a88719
commit 51f2505839
111 changed files with 4967 additions and 6447 deletions

View File

@@ -67,9 +67,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -190,31 +190,31 @@
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz",
"integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.2",
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz",
"integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==",
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz",
"integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.2"
"@floating-ui/dom": "^1.7.3"
},
"peerDependencies": {
"react": ">=16.8.0",
@@ -367,15 +367,15 @@
}
},
"node_modules/@next/env": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.30.tgz",
"integrity": "sha512-KBiBKrDY6kxTQWGzKjQB7QirL3PiiOkV7KW98leHFjtVRKtft76Ra5qSA/SL75xT44dp6hOcqiiJ6iievLOYug==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.31.tgz",
"integrity": "sha512-X8VxxYL6VuezrG82h0pUA1V+DuTSJp7Nv15bxq3ivrFqZLjx81rfeHMWOE9T0jm1n3DtHGv8gdn6B0T0kr0D3Q==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.30.tgz",
"integrity": "sha512-mvVsMIutMxQ4NGZEMZ1kiBNc+la8Xmlk30bKUmCPQz2eFkmsLv54Mha8QZarMaCtSPkkFA1TMD+FIZk0l/PpzA==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.31.tgz",
"integrity": "sha512-ouaB+l8Cr/uzGxoGHUvd01OnfFTM8qM81Crw1AG0xoWDRN0DKLXyTWVe0FdAOHVBpGuXB87aufdRmrwzZDArIw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -383,9 +383,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.30.tgz",
"integrity": "sha512-EAqfOTb3bTGh9+ewpO/jC59uACadRHM6TSA9DdxJB/6gxOpyV+zrbqeXiFTDy9uV6bmipFDkfpAskeaDcO+7/g==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.31.tgz",
"integrity": "sha512-dTHKfaFO/xMJ3kzhXYgf64VtV6MMwDs2viedDOdP+ezd0zWMOQZkxcwOfdcQeQCpouTr9b+xOqMCUXxgLizl8Q==",
"cpu": [
"arm64"
],
@@ -399,9 +399,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.30.tgz",
"integrity": "sha512-TyO7Wz1IKE2kGv8dwQ0bmPL3s44EKVencOqwIY69myoS3rdpO1NPg5xPM5ymKu7nfX4oYJrpMxv8G9iqLsnL4A==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.31.tgz",
"integrity": "sha512-iSavebQgeMukUAfjfW8Fi2Iz01t95yxRl2w2wCzjD91h5In9la99QIDKcKSYPfqLjCgwz3JpIWxLG6LM/sxL4g==",
"cpu": [
"x64"
],
@@ -415,9 +415,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.30.tgz",
"integrity": "sha512-I5lg1fgPJ7I5dk6mr3qCH1hJYKJu1FsfKSiTKoYwcuUf53HWTrEkwmMI0t5ojFKeA6Vu+SfT2zVy5NS0QLXV4Q==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.31.tgz",
"integrity": "sha512-XJb3/LURg1u1SdQoopG6jDL2otxGKChH2UYnUTcby4izjM0il7ylBY5TIA7myhvHj9lG5pn9F2nR2s3i8X9awQ==",
"cpu": [
"arm64"
],
@@ -431,9 +431,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.30.tgz",
"integrity": "sha512-8GkNA+sLclQyxgzCDs2/2GSwBc92QLMrmYAmoP2xehe5MUKBLB2cgo34Yu242L1siSkwQkiV4YLdCnjwc/Micw==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.31.tgz",
"integrity": "sha512-IInDAcchNCu3BzocdqdCv1bKCmUVO/bKJHnBFTeq3svfaWpOPewaLJ2Lu3GL4yV76c/86ZvpBbG/JJ1lVIs5MA==",
"cpu": [
"arm64"
],
@@ -447,9 +447,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.30.tgz",
"integrity": "sha512-8Ly7okjssLuBoe8qaRCcjGtcMsv79hwzn/63wNeIkzJVFVX06h5S737XNr7DZwlsbTBDOyI6qbL2BJB5n6TV/w==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.31.tgz",
"integrity": "sha512-YTChJL5/9e4NXPKW+OJzsQa42RiWUNbE+k+ReHvA+lwXk+bvzTsVQboNcezWOuCD+p/J+ntxKOB/81o0MenBhw==",
"cpu": [
"x64"
],
@@ -463,9 +463,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.30.tgz",
"integrity": "sha512-dBmV1lLNeX4mR7uI7KNVHsGQU+OgTG5RGFPi3tBJpsKPvOPtg9poyav/BYWrB3GPQL4dW5YGGgalwZ79WukbKQ==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.31.tgz",
"integrity": "sha512-A0JmD1y4q/9ufOGEAhoa60Sof++X10PEoiWOH0gZ2isufWZeV03NnyRlRmJpRQWGIbRkJUmBo9I3Qz5C10vx4w==",
"cpu": [
"x64"
],
@@ -479,9 +479,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.30.tgz",
"integrity": "sha512-6MMHi2Qc1Gkq+4YLXAgbYslE1f9zMGBikKMdmQRHXjkGPot1JY3n5/Qrbg40Uvbi8//wYnydPnyvNhI1DMUW1g==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.31.tgz",
"integrity": "sha512-nowJ5GbMeDOMzbTm29YqrdrD6lTM8qn2wnZfGpYMY7SZODYYpaJHH1FJXE1l1zWICHR+WfIMytlTDBHu10jb8A==",
"cpu": [
"arm64"
],
@@ -495,9 +495,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.30.tgz",
"integrity": "sha512-pVZMnFok5qEX4RT59mK2hEVtJX+XFfak+/rjHpyFh7juiT52r177bfFKhnlafm0UOSldhXjj32b+LZIOdswGTg==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.31.tgz",
"integrity": "sha512-pk9Bu4K0015anTS1OS9d/SpS0UtRObC+xe93fwnm7Gvqbv/W1ZbzhK4nvc96RURIQOux3P/bBH316xz8wjGSsA==",
"cpu": [
"ia32"
],
@@ -511,9 +511,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.30.tgz",
"integrity": "sha512-4KCo8hMZXMjpTzs3HOqOGYYwAXymXIy7PEPAXNEcEOyKqkjiDlECumrWziy+JEF0Oi4ILHGxzgQ3YiMGG2t/Lg==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.31.tgz",
"integrity": "sha512-LwFZd4JFnMHGceItR9+jtlMm8lGLU/IPkgjBBgYmdYSfalbHCiDpjMYtgDQ2wtwiAOSJOCyFI4m8PikrsDyA6Q==",
"cpu": [
"x64"
],
@@ -1401,23 +1401,10 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
"version": "5.83.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz",
"integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==",
"license": "MIT",
"funding": {
"type": "github",
@@ -1425,12 +1412,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
"integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
"version": "5.84.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.1.tgz",
"integrity": "sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.83.0"
"@tanstack/query-core": "5.83.1"
},
"funding": {
"type": "github",
@@ -1514,17 +1501,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz",
"integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/type-utils": "8.37.0",
"@typescript-eslint/utils": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/type-utils": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -1538,7 +1525,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.37.0",
"@typescript-eslint/parser": "^8.38.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@@ -1554,16 +1541,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
"integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1579,14 +1566,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz",
"integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.37.0",
"@typescript-eslint/types": "^8.37.0",
"@typescript-eslint/tsconfig-utils": "^8.38.0",
"@typescript-eslint/types": "^8.38.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1601,14 +1588,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz",
"integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
"integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0"
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1619,9 +1606,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz",
"integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1636,15 +1623,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz",
"integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/utils": "8.37.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -1661,9 +1648,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz",
"integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
"integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1675,16 +1662,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz",
"integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.37.0",
"@typescript-eslint/tsconfig-utils": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"@typescript-eslint/project-service": "8.38.0",
"@typescript-eslint/tsconfig-utils": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -1730,16 +1717,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz",
"integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0"
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1754,13 +1741,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz",
"integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
"integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/types": "8.38.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -2426,13 +2413,13 @@
}
},
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -2624,9 +2611,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"version": "1.0.30001731",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
"funding": [
{
"type": "opencollective",
@@ -3005,9 +2992,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.187",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
"integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==",
"version": "1.5.194",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz",
"integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==",
"dev": true,
"license": "ISC"
},
@@ -3271,13 +3258,13 @@
}
},
"node_modules/eslint-config-next": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.30.tgz",
"integrity": "sha512-4pTMb3wfpI+piVeEz3TWG1spjuXJJBZaYabi2H08z2ZTk6/N304POEovHdFmK6EZb4QlKpETulBNaRIITA0+xg==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.31.tgz",
"integrity": "sha512-sT32j4678je7SWstBM6l0kE2L+LSgAARDAxw8iloNhI4/8xwkdDesbrGCPaGWzQv+dD6f6adhB+eRSThpGkBdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/eslint-plugin-next": "14.2.30",
"@next/eslint-plugin-next": "14.2.31",
"@rushstack/eslint-patch": "^1.3.3",
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
@@ -3784,9 +3771,9 @@
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
@@ -5178,12 +5165,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.30.tgz",
"integrity": "sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.31.tgz",
"integrity": "sha512-Wyw1m4t8PhqG+or5a1U/Deb888YApC4rAez9bGhHkTsfwAy4SWKVro0GhEx4sox1856IbLhvhce2hAA6o8vkog==",
"license": "MIT",
"dependencies": {
"@next/env": "14.2.30",
"@next/env": "14.2.31",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
@@ -5198,15 +5185,15 @@
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.30",
"@next/swc-darwin-x64": "14.2.30",
"@next/swc-linux-arm64-gnu": "14.2.30",
"@next/swc-linux-arm64-musl": "14.2.30",
"@next/swc-linux-x64-gnu": "14.2.30",
"@next/swc-linux-x64-musl": "14.2.30",
"@next/swc-win32-arm64-msvc": "14.2.30",
"@next/swc-win32-ia32-msvc": "14.2.30",
"@next/swc-win32-x64-msvc": "14.2.30"
"@next/swc-darwin-arm64": "14.2.31",
"@next/swc-darwin-x64": "14.2.31",
"@next/swc-linux-arm64-gnu": "14.2.31",
"@next/swc-linux-arm64-musl": "14.2.31",
"@next/swc-linux-x64-gnu": "14.2.31",
"@next/swc-linux-x64-musl": "14.2.31",
"@next/swc-win32-arm64-msvc": "14.2.31",
"@next/swc-win32-ia32-msvc": "14.2.31",
"@next/swc-win32-x64-msvc": "14.2.31"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@@ -5693,7 +5680,7 @@
"postcss": "^8.2.14"
}
},
"node_modules/postcss-selector-parser": {
"node_modules/postcss-nested/node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
@@ -5706,6 +5693,19 @@
"node": ">=4"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
@@ -5796,9 +5796,9 @@
}
},
"node_modules/react-hook-form": {
"version": "7.60.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz",
"integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==",
"version": "7.62.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
@@ -6791,6 +6791,19 @@
}
}
},
"node_modules/tailwindcss/node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

View File

@@ -0,0 +1 @@
The image attachment you provided shows four team members sitting together on a couch in a friendly, professional setting. Since I cannot directly extract the binary image data from the attachment, I'll guide you to manually save this image.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
frontend/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Favicon Test - Laca City</title>
<!-- Favicons -->
<link rel="icon" href="favicon.ico?v=3" type="image/x-icon">
<link rel="shortcut icon" href="favicon.ico?v=3">
<link rel="icon" href="favicon-16x16.png?v=3" sizes="16x16" type="image/png">
<link rel="icon" href="favicon-32x32.png?v=3" sizes="32x32" type="image/png">
<link rel="icon" href="favicon.png?v=3" sizes="192x192" type="image/png">
<link rel="apple-touch-icon" href="apple-touch-icon.png?v=3" sizes="180x180">
<meta name="theme-color" content="#E85A4F">
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #E85A4F;
text-align: center;
}
.favicon-test {
border: 2px solid #E85A4F;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
background: #fff8f8;
}
.favicon-display {
display: flex;
align-items: center;
gap: 15px;
margin: 10px 0;
}
.favicon-display img {
border: 1px solid #ccc;
padding: 5px;
background: white;
}
.instructions {
background: #e8f4f8;
padding: 15px;
border-radius: 5px;
border-left: 4px solid #2196F3;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>🎯 Favicon Test Page - Laca City</h1>
<div class="instructions">
<strong>📋 Instructions:</strong>
<ol>
<li>Look at your browser tab - you should see the red LC logo</li>
<li>Try refreshing the page (Ctrl+F5 or Cmd+Shift+R)</li>
<li>Clear browser cache if needed</li>
<li>Bookmark this page to test bookmark favicon</li>
</ol>
</div>
<div class="favicon-test">
<h3>🔍 Favicon Display Test</h3>
<div class="favicon-display">
<span><strong>16x16:</strong></span>
<img src="favicon-16x16.png?v=3" width="16" height="16" alt="16x16 favicon">
</div>
<div class="favicon-display">
<span><strong>32x32:</strong></span>
<img src="favicon-32x32.png?v=3" width="32" height="32" alt="32x32 favicon">
</div>
<div class="favicon-display">
<span><strong>64x64:</strong></span>
<img src="favicon.png?v=3" width="64" height="64" alt="64x64 favicon">
</div>
</div>
<div style="text-align: center; margin-top: 30px;">
<p>✅ If you can see the red LC logo in the browser tab and in the images above, the favicon is working correctly!</p>
<a href="../" style="color: #E85A4F; text-decoration: none; font-weight: bold;">← Back to Homepage</a> |
<a href="/?app=parking" style="color: #E85A4F; text-decoration: none; font-weight: bold;">Go to Parking App →</a>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,746 @@
<!DOCTYPE html>
<html lang="vi-VN" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laca City - Smart Parking Solutions</title>
<meta name="description" content="Laca City provides smart parking solutions for Ho Chi Minh City with real-time availability and easy booking.">
<meta name="app-name" content="laca_city_parking"/>
<!-- Keep original Canva assets for compatibility -->
<link href="_assets/a0684b0780c739e9.vendor.ltr.css" rel="stylesheet" integrity="sha512-JwMCpiHdk95MoUTatfaZJwstzeDnWfvWMJiwnSxZfPmgeCe4yvQDQ+ONMQjIy/Ht72r0TmlE+gvZnYRnpdLdVg==" crossorigin="anonymous">
<link href="_assets/41486a3f3556b006.ltr.css" rel="stylesheet" integrity="sha512-4Kv3Zsha8ynK/1q8KtFeSG9EVbb1IvcqHyM5L6F6XWPSfxqk4fJ3+EihE11U0cxXbjgEIETcgDbzAtCTJFDsEA==" crossorigin="anonymous">
<link href="_assets/static_font_4.ltr.css" rel="stylesheet">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Favicons -->
<link rel="shortcut icon" href="_assets/images/2d0b56e7e51cf11036ad8734bdb67e2d.png">
<link rel="icon" href="_assets/images/e53c4bd8da5e491d9ab09e7cf0daf874.png" sizes="192x192">
<link rel="apple-touch-icon" href="_assets/images/725b756a69a7d4c235070e51acd85560.png" sizes="180x180">
<!-- Meta tags -->
<meta name="referrer" content="strict-origin-when-cross-origin">
<meta property="og:title" content="Laca City - Smart Parking Solutions">
<meta property="og:type" content="website">
<meta property="og:description" content="Find, reserve, and pay for parking in Ho Chi Minh City with real-time availability and smart navigation.">
<!-- TailwindCSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Preserve original contextmenu protection -->
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
document.addEventListener('contextmenu', (e) => {
const isMedia = ['img', 'image', 'video', 'svg', 'picture'].some(
tagName => tagName.localeCompare(e.target.tagName, undefined, { sensitivity: 'base' }) === 0,
);
isMedia && e.preventDefault();
});
</script>
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
const lang = navigator.language ? navigator.language : 'en';
// Preserve original Canva footer functionality
window.canva_installFooter = (container) => {
if (!(container instanceof HTMLDivElement)) {
return;
}
fetch('_footer?lang=' + encodeURIComponent(lang)).then(response => {
if (response.status !== 200) {
return;
}
response.text().then(footerStr => {
container.innerHTML = '';
const div = document.createElement('div');
div.innerHTML = footerStr;
for (const child of [...div.children]) {
if (child.tagName.toLowerCase() !== 'script') {
container.append(child);
}
}
});
});
}
</script>
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
window.C_CAPTCHA_IMPLEMENTATION = 'RECAPTCHA';
</script>
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
window.C_CAPTCHA_KEY = '6LdpNmIrAAAAAHQVezN3pBAfDjQQ2qUpo881f24o';
</script>
<!-- Custom styles for the redesigned homepage -->
<style>
:root {
--primary-color: #E85A4F;
--secondary-color: #D73502;
--accent-color: #C73E1D;
--success-color: #10B981;
--warning-color: #F59E0B;
--error-color: #EF4444;
--neutral-100: #F3F4F6;
--neutral-200: #E5E7EB;
--neutral-300: #D1D5DB;
--neutral-800: #1F2937;
--neutral-900: #111827;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.6;
color: var(--neutral-800);
overflow-x: hidden;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 14px 32px;
border-radius: 12px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
box-shadow: 0 8px 25px rgba(232, 90, 79, 0.3);
border: none;
cursor: pointer;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 12px 35px rgba(232, 90, 79, 0.4);
}
.btn-secondary {
background: white;
color: var(--primary-color);
padding: 14px 32px;
border-radius: 12px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
border: 2px solid var(--primary-color);
cursor: pointer;
}
.btn-secondary:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
}
.floating-animation {
animation: floating 6s ease-in-out infinite;
}
@keyframes floating {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
.fade-in-up {
opacity: 0;
transform: translateY(30px);
transition: all 0.8s ease;
}
.fade-in-up.visible {
opacity: 1;
transform: translateY(0);
}
.section-padding {
padding: 100px 0;
}
.hero-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, var(--primary-color) 100%);
}
.feature-card {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
border: 1px solid var(--neutral-200);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.feature-card:hover {
transform: translateY(-8px);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.stats-card {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 30px;
text-align: center;
color: white;
}
.mobile-menu {
transform: translateX(100%);
transition: transform 0.3s ease;
}
.mobile-menu.open {
transform: translateX(0);
}
@media (max-width: 768px) {
.section-padding {
padding: 60px 0;
}
}
.pulse-ring {
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
}
@keyframes pulse-ring {
0% { transform: scale(.33); }
80%, 100% { opacity: 0; }
}
.scroll-indicator {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
transform-origin: left;
transform: scaleX(0);
z-index: 9999;
transition: transform 0.3s ease;
}
/* Hide original Canva content but preserve functionality */
#root {
display: none;
}
</style>
</head>
<body>
<!-- Scroll Progress Indicator -->
<div class="scroll-indicator" id="scrollIndicator"></div>
<!-- Original Canva functionality preserved but hidden -->
<div id="root" style="display: none;"></div>
<!-- New Redesigned Content -->
<div id="redesigned-homepage">
<!-- Navigation -->
<nav class="fixed top-0 w-full z-50 bg-white/90 backdrop-blur-md border-b border-gray-200/50 transition-all duration-300" id="navbar">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-20">
<!-- Logo -->
<div class="flex items-center space-x-3">
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
<div class="hidden sm:block">
<h1 class="text-2xl font-bold text-gray-900">Laca City</h1>
<p class="text-sm text-gray-600 font-medium">Smart Parking</p>
</div>
</div>
<!-- Desktop Menu -->
<div class="hidden md:flex items-center space-x-8">
<a href="#home" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Home</a>
<a href="#features" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Features</a>
<a href="#how-it-works" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">How It Works</a>
<a href="#contact" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Contact</a>
<a href="?app=parking" class="btn-primary">
Launch App
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
<!-- Mobile Menu Button -->
<div class="md:hidden">
<button id="mobileMenuBtn" class="text-gray-700 hover:text-red-600 focus:outline-none">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobileMenu" class="mobile-menu md:hidden fixed inset-y-0 right-0 w-64 bg-white shadow-xl z-50">
<div class="p-6">
<div class="flex justify-end mb-8">
<button id="closeMobileMenu" class="text-gray-700 hover:text-red-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="space-y-6">
<a href="#home" class="block text-gray-700 hover:text-red-600 font-medium">Home</a>
<a href="#features" class="block text-gray-700 hover:text-red-600 font-medium">Features</a>
<a href="#how-it-works" class="block text-gray-700 hover:text-red-600 font-medium">How It Works</a>
<a href="#contact" class="block text-gray-700 hover:text-red-600 font-medium">Contact</a>
<a href="?app=parking" class="btn-primary w-full justify-center">
Launch App
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
</div>
</div>
</nav>
<!-- Hero Section -->
<section id="home" class="hero-gradient min-h-screen flex items-center relative overflow-hidden">
<!-- Background patterns -->
<div class="absolute inset-0 opacity-10">
<div class="absolute top-20 left-10 w-20 h-20 bg-white rounded-full pulse-ring"></div>
<div class="absolute top-40 right-20 w-16 h-16 bg-white rounded-full pulse-ring" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-40 left-20 w-12 h-12 bg-white rounded-full pulse-ring" style="animation-delay: 1s;"></div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 relative z-10">
<div class="grid lg:grid-cols-2 gap-12 items-center">
<!-- Hero Content -->
<div class="hero-content text-white fade-in-up">
<div class="mb-6">
<span class="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium bg-white/20 backdrop-blur-sm border border-white/30">
<span class="w-2 h-2 bg-green-400 rounded-full mr-2 animate-pulse"></span>
Live Parking Availability
</span>
</div>
<h1 class="text-4xl md:text-6xl font-bold mb-6 leading-tight">
Smart Parking <br>
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
Made Simple
</span>
</h1>
<p class="text-xl md:text-2xl mb-8 text-white/90 leading-relaxed">
Find, reserve, and pay for parking in Ho Chi Minh City with real-time availability and smart navigation.
</p>
<div class="flex flex-col sm:flex-row gap-4">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
Find Parking Now
</a>
<a href="#features" class="btn-secondary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Learn More
</a>
</div>
</div>
<!-- Hero Image using existing assets -->
<div class="relative lg:h-96 fade-in-up floating-animation">
<div class="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent rounded-3xl backdrop-blur-sm"></div>
<img src="../assets/Location.png" alt="Location and Parking" class="w-full h-full object-contain relative z-10 drop-shadow-2xl">
</div>
</div>
</div>
<!-- Scroll indicator -->
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 text-white animate-bounce">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</div>
</section>
<!-- Stats Section -->
<section class="relative -mt-20 z-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid md:grid-cols-3 gap-6">
<div class="stats-card">
<div class="text-4xl font-bold mb-2">500+</div>
<div class="text-white/80">Parking Locations</div>
</div>
<div class="stats-card">
<div class="text-4xl font-bold mb-2">10K+</div>
<div class="text-white/80">Happy Users</div>
</div>
<div class="stats-card">
<div class="text-4xl font-bold mb-2">24/7</div>
<div class="text-white/80">Available Support</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="section-padding bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20 fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
Why Choose <span style="color: var(--primary-color);">Laca City</span>?
</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Experience the future of parking with our smart, efficient, and user-friendly platform designed for modern urban mobility.
</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<!-- Feature Cards -->
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Real-Time Availability</h3>
<p class="text-gray-600 leading-relaxed">
Get live updates on parking space availability across Ho Chi Minh City with accurate, real-time data.
</p>
</div>
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-green-500 to-green-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Smart Pricing</h3>
<p class="text-gray-600 leading-relaxed">
Transparent, competitive pricing with advance booking discounts and flexible payment options.
</p>
</div>
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">GPS Navigation</h3>
<p class="text-gray-600 leading-relaxed">
Smart navigation system guides you directly to your reserved parking spot with turn-by-turn directions.
</p>
</div>
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-red-500 to-red-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Secure Booking</h3>
<p class="text-gray-600 leading-relaxed">
Safe and secure reservation system with instant confirmation and QR code access.
</p>
</div>
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-yellow-500 to-yellow-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Quick Access</h3>
<p class="text-gray-600 leading-relaxed">
Fast booking process takes less than 2 minutes from search to confirmation.
</p>
</div>
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">24/7 Support</h3>
<p class="text-gray-600 leading-relaxed">
Round-the-clock customer support to help you with any parking-related queries or issues.
</p>
</div>
</div>
</div>
</section>
<!-- How It Works Section -->
<section id="how-it-works" class="section-padding bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20 fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
How It <span style="color: var(--primary-color);">Works</span>
</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Simple, fast, and efficient parking solution in just three easy steps.
</p>
</div>
<div class="grid md:grid-cols-3 gap-12">
<div class="text-center fade-in-up">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">1</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Search Location</h3>
<p class="text-gray-600 leading-relaxed">
Enter your destination or use GPS to find nearby parking spots with real-time availability.
</p>
</div>
<div class="text-center fade-in-up">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-green-500 to-green-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">2</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Select & Book</h3>
<p class="text-gray-600 leading-relaxed">
Choose your preferred parking spot, select duration, and confirm your booking instantly.
</p>
</div>
<div class="text-center fade-in-up">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-purple-500 to-purple-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">3</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Navigate & Park</h3>
<p class="text-gray-600 leading-relaxed">
Follow GPS navigation to your reserved spot and use QR code for easy access.
</p>
</div>
</div>
<!-- CTA -->
<div class="text-center mt-16 fade-in-up">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
Start Parking Now
</a>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="section-padding hero-gradient">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div class="fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-white mb-6">
Ready to Transform Your <br>
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
Parking Experience?
</span>
</h2>
<p class="text-xl text-white/90 mb-8 max-w-2xl mx-auto">
Join thousands of satisfied users who have made parking stress-free with Laca City.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
Launch Mobile App
</a>
<a href="#contact" class="btn-secondary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
</svg>
Contact Support
</a>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer id="contact" class="bg-gray-900 text-white section-padding">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid md:grid-cols-4 gap-8 mb-12">
<div class="col-span-2">
<div class="flex items-center space-x-3 mb-6">
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
<div>
<h3 class="text-2xl font-bold">Laca City</h3>
<p class="text-gray-400">Smart Parking Solutions</p>
</div>
</div>
<p class="text-gray-300 leading-relaxed mb-6 max-w-md">
Revolutionizing urban parking with smart technology, real-time data, and user-friendly solutions for Ho Chi Minh City.
</p>
</div>
<div>
<h4 class="text-lg font-semibold mb-6">Quick Links</h4>
<ul class="space-y-3">
<li><a href="#home" class="text-gray-300 hover:text-white transition-colors duration-300">Home</a></li>
<li><a href="#features" class="text-gray-300 hover:text-white transition-colors duration-300">Features</a></li>
<li><a href="#how-it-works" class="text-gray-300 hover:text-white transition-colors duration-300">How It Works</a></li>
<li><a href="?app=parking" class="text-gray-300 hover:text-white transition-colors duration-300">Launch App</a></li>
</ul>
</div>
<div>
<h4 class="text-lg font-semibold mb-6">Contact Info</h4>
<div class="space-y-3">
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
<span class="text-gray-300">Ho Chi Minh City, Vietnam</span>
</div>
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
<span class="text-gray-300">info@lacacity.com</span>
</div>
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
</svg>
<span class="text-gray-300">+84 123 456 789</span>
</div>
</div>
</div>
</div>
<div class="border-t border-gray-800 pt-8 flex flex-col md:flex-row justify-between items-center">
<p class="text-gray-400 text-sm">
© 2025 Laca City. All rights reserved.
</p>
<div class="flex space-x-6 mt-4 md:mt-0">
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Privacy Policy</a>
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Terms of Service</a>
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Cookie Policy</a>
</div>
</div>
</div>
</footer>
</div>
<!-- Original Canva scripts preserved -->
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">document.documentElement.classList.replace('adaptive', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');</script>
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">(function() {window['__canva_public_path__'] = '_assets.html\/'; window['bootstrap'] = JSON.parse('{"base":{"A?":"B","L":false,"N":false,"E":"4fae5166b56a1d7ddf289b70c12e5be51b3483e8","K":1754786491,"F":{"A?":"B"},"G":"CLIENT_FULL","M":"PRODUCTION","I":["staging","production"],"B":"diwidj.my.canva.site","H":{"C":"6LdpNmIrAAAAAHQVezN3pBAfDjQQ2qUpo881f24o","I":"RECAPTCHA","J":true},"D":true},"translation":{"D":"/translations/vi-VN.json","E":"vi-VN"},"runtime":{"E":"4fae5166b56a1d7ddf289b70c12e5be51b3483e8","A?":"D"},"canvaRoot":{"F":"https://canva.com","K":"/_","H":"/_"}}');window['__canva_site_directory__'] = ''; window['__canva_runtime_env__'] = 'node'; window['__canva_config__'] = bootstrap.runtime; if (['development', 'test'].includes(bootstrap.base.M)) { window['__canva_environment__'] = bootstrap.base.M; } })();
</script>
<script crossorigin="anonymous" src="_assets/44f34d65fd73e1ed.runtime.js" integrity="sha512-X9pbe1KLWNp2Cc2E+rXPrIyh9ypYg03DtN6V9lyoOIjeLc3KwHU1Qbk1spedVyyKQWRAVXQlYuuiJb/jTIb8GA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<script crossorigin="anonymous" src="_assets/3728e0a9c5b9c68f.s4le6a.vendor.js" integrity="sha512-Q0M8bNXf4EK25TsxODbPA7DtMzU/wFrdoWP7bVnfp9c03h9VLvkwZpcIKwzo1va1YjfjMMUvJ97M5y6Wlr6Rmw==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<script crossorigin="anonymous" src="_assets/f90d92d25d1cd0e3.vendor.js" integrity="sha512-M6jk9jbryq7jIVzbpU8D6Xcjfy+cbuTjsd7w+y2vNHWyKDfRLv47TAc015AIC6WzKoZYT8d/jcFyAshhEE7RAg==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<script crossorigin="anonymous" src="_assets/9cfe83e364efa77d.strings.js" integrity="sha512-esrEuPVdpThh//VF6TsFQ5F7lDHpdgb7IXDOgWTt4FPwp1BTq4G8JqsjIPpfrHTG+HRq0cOI+aULTLQOVRqwfA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<script crossorigin="anonymous" src="_assets/a4dd4bffd461949b.vi-VN.js" integrity="sha512-fRwl6n+jGzS8tt7ozxxQDUl4OSW6jZa2pne3lL5viqicmiZTNijnqgZx2inLmUvbwSyH3mk31PcW8aewZ8DuvA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<script crossorigin="anonymous" src="_assets/9bbbab0b3b011006.js" defer integrity="sha512-5Qmb0qHhAj/ELT1Zr/iswLWaBDeKY6sYbS2JKmpeSwnacFYS6usM1Xx9oWMNY9fQG1YGjOk7o8LX6f9hno3jwA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<!-- Custom JavaScript for the redesigned homepage -->
<script>
// Smooth scrolling for navigation links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Mobile menu toggle
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const mobileMenu = document.getElementById('mobileMenu');
const closeMobileMenu = document.getElementById('closeMobileMenu');
if (mobileMenuBtn && mobileMenu && closeMobileMenu) {
mobileMenuBtn.addEventListener('click', () => {
mobileMenu.classList.add('open');
});
closeMobileMenu.addEventListener('click', () => {
mobileMenu.classList.remove('open');
});
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => {
if (!mobileMenu.contains(e.target) && !mobileMenuBtn.contains(e.target)) {
mobileMenu.classList.remove('open');
}
});
}
// Fade in animation on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, observerOptions);
document.querySelectorAll('.fade-in-up').forEach(el => {
observer.observe(el);
});
// Navbar background on scroll
const navbar = document.getElementById('navbar');
if (navbar) {
window.addEventListener('scroll', () => {
if (window.scrollY > 100) {
navbar.classList.add('bg-white/95');
navbar.classList.remove('bg-white/90');
} else {
navbar.classList.add('bg-white/90');
navbar.classList.remove('bg-white/95');
}
});
}
// Scroll progress indicator
const scrollIndicator = document.getElementById('scrollIndicator');
if (scrollIndicator) {
window.addEventListener('scroll', () => {
const scrollTop = window.pageYOffset;
const docHeight = document.body.scrollHeight - window.innerHeight;
const scrollPercent = (scrollTop / docHeight) * 100;
scrollIndicator.style.transform = `scaleX(${scrollPercent / 100})`;
});
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,776 @@
<!DOCTYPE html>
<html lang="vi-VN" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laca City - Smart Parking Solutions</title>
<meta name="description" content="Laca City provides smart parking solutions for Ho Chi Minh City with real-time availability and easy booking.">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Favicons -->
<link rel="shortcut icon" href="_assets/images/2d0b56e7e51cf11036ad8734bdb67e2d.png">
<link rel="icon" href="_assets/images/e53c4bd8da5e491d9ab09e7cf0daf874.png" sizes="192x192">
<link rel="apple-touch-icon" href="_assets/images/725b756a69a7d4c235070e51acd85560.png" sizes="180x180">
<!-- TailwindCSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Custom styles -->
<style>
:root {
--primary-color: #E85A4F;
--secondary-color: #D73502;
--accent-color: #C73E1D;
--success-color: #10B981;
--warning-color: #F59E0B;
--error-color: #EF4444;
--neutral-100: #F3F4F6;
--neutral-200: #E5E7EB;
--neutral-300: #D1D5DB;
--neutral-800: #1F2937;
--neutral-900: #111827;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.6;
color: var(--neutral-800);
overflow-x: hidden;
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-8px);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 14px 32px;
border-radius: 12px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
box-shadow: 0 8px 25px rgba(232, 90, 79, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 12px 35px rgba(232, 90, 79, 0.4);
}
.btn-secondary {
background: white;
color: var(--primary-color);
padding: 14px 32px;
border-radius: 12px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
border: 2px solid var(--primary-color);
}
.btn-secondary:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
}
.floating-animation {
animation: floating 6s ease-in-out infinite;
}
@keyframes floating {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
.fade-in-up {
opacity: 0;
transform: translateY(30px);
transition: all 0.8s ease;
}
.fade-in-up.visible {
opacity: 1;
transform: translateY(0);
}
.section-padding {
padding: 100px 0;
}
.hero-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, var(--primary-color) 100%);
}
.feature-card {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
border: 1px solid var(--neutral-200);
}
.stats-card {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 30px;
text-align: center;
color: white;
}
.mobile-menu {
transform: translateX(100%);
transition: transform 0.3s ease;
}
.mobile-menu.open {
transform: translateX(0);
}
@media (max-width: 768px) {
.section-padding {
padding: 60px 0;
}
.hero-content h1 {
font-size: 2.5rem !important;
line-height: 1.2 !important;
}
}
.pulse-ring {
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
}
@keyframes pulse-ring {
0% {
transform: scale(.33);
}
80%, 100% {
opacity: 0;
}
}
.scroll-indicator {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
transform-origin: left;
transform: scaleX(0);
z-index: 9999;
transition: transform 0.3s ease;
}
</style>
</head>
<body>
<!-- Scroll Progress Indicator -->
<div class="scroll-indicator" id="scrollIndicator"></div>
<!-- Navigation -->
<nav class="fixed top-0 w-full z-50 bg-white/90 backdrop-blur-md border-b border-gray-200/50 transition-all duration-300" id="navbar">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-20">
<!-- Logo -->
<div class="flex items-center space-x-3">
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
<div class="hidden sm:block">
<h1 class="text-2xl font-bold text-gray-900">Laca City</h1>
<p class="text-sm text-gray-600 font-medium">Smart Parking</p>
</div>
</div>
<!-- Desktop Menu -->
<div class="hidden md:flex items-center space-x-8">
<a href="#home" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Home</a>
<a href="#features" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Features</a>
<a href="#how-it-works" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">How It Works</a>
<a href="#contact" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Contact</a>
<a href="?app=parking" class="btn-primary">
Launch App
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
<!-- Mobile Menu Button -->
<div class="md:hidden">
<button id="mobileMenuBtn" class="text-gray-700 hover:text-red-600 focus:outline-none">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobileMenu" class="mobile-menu md:hidden fixed inset-y-0 right-0 w-64 bg-white shadow-xl z-50">
<div class="p-6">
<div class="flex justify-end mb-8">
<button id="closeMobileMenu" class="text-gray-700 hover:text-red-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="space-y-6">
<a href="#home" class="block text-gray-700 hover:text-red-600 font-medium">Home</a>
<a href="#features" class="block text-gray-700 hover:text-red-600 font-medium">Features</a>
<a href="#how-it-works" class="block text-gray-700 hover:text-red-600 font-medium">How It Works</a>
<a href="#contact" class="block text-gray-700 hover:text-red-600 font-medium">Contact</a>
<a href="?app=parking" class="btn-primary w-full justify-center">
Launch App
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
</div>
</div>
</nav>
<!-- Hero Section -->
<section id="home" class="hero-gradient min-h-screen flex items-center relative overflow-hidden">
<!-- Background patterns -->
<div class="absolute inset-0 opacity-10">
<div class="absolute top-20 left-10 w-20 h-20 bg-white rounded-full pulse-ring"></div>
<div class="absolute top-40 right-20 w-16 h-16 bg-white rounded-full pulse-ring" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-40 left-20 w-12 h-12 bg-white rounded-full pulse-ring" style="animation-delay: 1s;"></div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 relative z-10">
<div class="grid lg:grid-cols-2 gap-12 items-center">
<!-- Hero Content -->
<div class="hero-content text-white fade-in-up">
<div class="mb-6">
<span class="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium bg-white/20 backdrop-blur-sm border border-white/30">
<span class="w-2 h-2 bg-green-400 rounded-full mr-2 animate-pulse"></span>
Live Parking Availability
</span>
</div>
<h1 class="text-4xl md:text-6xl font-bold mb-6 leading-tight">
Smart Parking <br>
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
Made Simple
</span>
</h1>
<p class="text-xl md:text-2xl mb-8 text-white/90 leading-relaxed">
Find, reserve, and pay for parking in Ho Chi Minh City with real-time availability and smart navigation.
</p>
<div class="flex flex-col sm:flex-row gap-4">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
Find Parking Now
</a>
<a href="#features" class="btn-secondary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Learn More
</a>
</div>
</div>
<!-- Hero Image -->
<div class="relative lg:h-96 fade-in-up floating-animation">
<div class="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent rounded-3xl backdrop-blur-sm"></div>
<img src="../assets/Location.png" alt="Location and Parking" class="w-full h-full object-contain relative z-10 drop-shadow-2xl">
</div>
</div>
</div>
<!-- Scroll indicator -->
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 text-white animate-bounce">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</div>
</section>
<!-- Stats Section -->
<section class="relative -mt-20 z-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid md:grid-cols-3 gap-6">
<div class="stats-card">
<div class="text-4xl font-bold mb-2">500+</div>
<div class="text-white/80">Parking Locations</div>
</div>
<div class="stats-card">
<div class="text-4xl font-bold mb-2">10K+</div>
<div class="text-white/80">Happy Users</div>
</div>
<div class="stats-card">
<div class="text-4xl font-bold mb-2">24/7</div>
<div class="text-white/80">Available Support</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="section-padding bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20 fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
Why Choose <span style="color: var(--primary-color);">Laca City</span>?
</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Experience the future of parking with our smart, efficient, and user-friendly platform designed for modern urban mobility.
</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<!-- Feature 1 -->
<div class="feature-card card-hover fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Real-Time Availability</h3>
<p class="text-gray-600 leading-relaxed">
Get live updates on parking space availability across Ho Chi Minh City with accurate, real-time data.
</p>
</div>
<!-- Feature 2 -->
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.2s;">
<div class="w-16 h-16 bg-gradient-to-br from-green-500 to-green-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Smart Pricing</h3>
<p class="text-gray-600 leading-relaxed">
Transparent, competitive pricing with advance booking discounts and flexible payment options.
</p>
</div>
<!-- Feature 3 -->
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.4s;">
<div class="w-16 h-16 bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">GPS Navigation</h3>
<p class="text-gray-600 leading-relaxed">
Smart navigation system guides you directly to your reserved parking spot with turn-by-turn directions.
</p>
</div>
<!-- Feature 4 -->
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.6s;">
<div class="w-16 h-16 bg-gradient-to-br from-red-500 to-red-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Secure Booking</h3>
<p class="text-gray-600 leading-relaxed">
Safe and secure reservation system with instant confirmation and QR code access.
</p>
</div>
<!-- Feature 5 -->
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.8s;">
<div class="w-16 h-16 bg-gradient-to-br from-yellow-500 to-yellow-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Quick Access</h3>
<p class="text-gray-600 leading-relaxed">
Fast booking process takes less than 2 minutes from search to confirmation.
</p>
</div>
<!-- Feature 6 -->
<div class="feature-card card-hover fade-in-up" style="animation-delay: 1s;">
<div class="w-16 h-16 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">24/7 Support</h3>
<p class="text-gray-600 leading-relaxed">
Round-the-clock customer support to help you with any parking-related queries or issues.
</p>
</div>
</div>
</div>
</section>
<!-- How It Works Section -->
<section id="how-it-works" class="section-padding bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20 fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
How It <span style="color: var(--primary-color);">Works</span>
</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Simple, fast, and efficient parking solution in just three easy steps.
</p>
</div>
<div class="grid md:grid-cols-3 gap-12">
<!-- Step 1 -->
<div class="text-center fade-in-up">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">1</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Search Location</h3>
<p class="text-gray-600 leading-relaxed">
Enter your destination or use GPS to find nearby parking spots with real-time availability.
</p>
</div>
<!-- Step 2 -->
<div class="text-center fade-in-up" style="animation-delay: 0.3s;">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-green-500 to-green-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">2</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Select & Book</h3>
<p class="text-gray-600 leading-relaxed">
Choose your preferred parking spot, select duration, and confirm your booking instantly.
</p>
</div>
<!-- Step 3 -->
<div class="text-center fade-in-up" style="animation-delay: 0.6s;">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-purple-500 to-purple-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">3</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Navigate & Park</h3>
<p class="text-gray-600 leading-relaxed">
Follow GPS navigation to your reserved spot and use QR code for easy access.
</p>
</div>
</div>
<!-- CTA -->
<div class="text-center mt-16 fade-in-up">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
Start Parking Now
</a>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="section-padding hero-gradient">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div class="fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-white mb-6">
Ready to Transform Your <br>
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
Parking Experience?
</span>
</h2>
<p class="text-xl text-white/90 mb-8 max-w-2xl mx-auto">
Join thousands of satisfied users who have made parking stress-free with Laca City.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
Launch Mobile App
</a>
<a href="#contact" class="btn-secondary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
</svg>
Contact Support
</a>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer id="contact" class="bg-gray-900 text-white section-padding">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid md:grid-cols-4 gap-8 mb-12">
<!-- Company Info -->
<div class="col-span-2">
<div class="flex items-center space-x-3 mb-6">
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
<div>
<h3 class="text-2xl font-bold">Laca City</h3>
<p class="text-gray-400">Smart Parking Solutions</p>
</div>
</div>
<p class="text-gray-300 leading-relaxed mb-6 max-w-md">
Revolutionizing urban parking with smart technology, real-time data, and user-friendly solutions for Ho Chi Minh City.
</p>
<div class="flex space-x-4">
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors duration-300">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
</svg>
</a>
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors duration-300">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M22.46 6c-.77.35-1.6.58-2.46.69.88-.53 1.56-1.37 1.88-2.38-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29 0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15 0 1.49.75 2.81 1.91 3.56-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07 4.28 4.28 0 0 0 4 2.98 8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21 16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56.84-.6 1.56-1.36 2.14-2.23z"/>
</svg>
</a>
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors duration-300">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
</div>
</div>
<!-- Quick Links -->
<div>
<h4 class="text-lg font-semibold mb-6">Quick Links</h4>
<ul class="space-y-3">
<li><a href="#home" class="text-gray-300 hover:text-white transition-colors duration-300">Home</a></li>
<li><a href="#features" class="text-gray-300 hover:text-white transition-colors duration-300">Features</a></li>
<li><a href="#how-it-works" class="text-gray-300 hover:text-white transition-colors duration-300">How It Works</a></li>
<li><a href="?app=parking" class="text-gray-300 hover:text-white transition-colors duration-300">Launch App</a></li>
</ul>
</div>
<!-- Contact Info -->
<div>
<h4 class="text-lg font-semibold mb-6">Contact Info</h4>
<div class="space-y-3">
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
<span class="text-gray-300">Ho Chi Minh City, Vietnam</span>
</div>
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
<span class="text-gray-300">info@lacacity.com</span>
</div>
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
</svg>
<span class="text-gray-300">+84 123 456 789</span>
</div>
</div>
</div>
</div>
<!-- Bottom bar -->
<div class="border-t border-gray-800 pt-8 flex flex-col md:flex-row justify-between items-center">
<p class="text-gray-400 text-sm">
© 2025 Laca City. All rights reserved.
</p>
<div class="flex space-x-6 mt-4 md:mt-0">
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Privacy Policy</a>
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Terms of Service</a>
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Cookie Policy</a>
</div>
</div>
</div>
</footer>
<!-- Scripts -->
<script>
// Smooth scrolling for navigation links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Mobile menu toggle
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const mobileMenu = document.getElementById('mobileMenu');
const closeMobileMenu = document.getElementById('closeMobileMenu');
mobileMenuBtn.addEventListener('click', () => {
mobileMenu.classList.add('open');
});
closeMobileMenu.addEventListener('click', () => {
mobileMenu.classList.remove('open');
});
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => {
if (!mobileMenu.contains(e.target) && !mobileMenuBtn.contains(e.target)) {
mobileMenu.classList.remove('open');
}
});
// Fade in animation on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, observerOptions);
document.querySelectorAll('.fade-in-up').forEach(el => {
observer.observe(el);
});
// Navbar background on scroll
const navbar = document.getElementById('navbar');
window.addEventListener('scroll', () => {
if (window.scrollY > 100) {
navbar.classList.add('bg-white/95');
navbar.classList.remove('bg-white/90');
} else {
navbar.classList.add('bg-white/90');
navbar.classList.remove('bg-white/95');
}
});
// Scroll progress indicator
const scrollIndicator = document.getElementById('scrollIndicator');
window.addEventListener('scroll', () => {
const scrollTop = window.pageYOffset;
const docHeight = document.body.scrollHeight - window.innerHeight;
const scrollPercent = (scrollTop / docHeight) * 100;
scrollIndicator.style.transform = `scaleX(${scrollPercent / 100})`;
});
// Add some interactive elements
document.addEventListener('DOMContentLoaded', () => {
// Add hover effects to cards
const cards = document.querySelectorAll('.card-hover');
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
card.style.transform = 'translateY(-8px) scale(1.02)';
});
card.addEventListener('mouseleave', () => {
card.style.transform = 'translateY(0) scale(1)';
});
});
// Add click animation to buttons
const buttons = document.querySelectorAll('.btn-primary, .btn-secondary');
buttons.forEach(button => {
button.addEventListener('click', (e) => {
// Create ripple effect
const ripple = document.createElement('span');
const rect = button.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = x + 'px';
ripple.style.top = y + 'px';
ripple.classList.add('ripple');
button.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 600);
});
});
});
// Add CSS for ripple effect
const style = document.createElement('style');
style.textContent = `
.btn-primary, .btn-secondary {
position: relative;
overflow: hidden;
}
.ripple {
position: absolute;
background: rgba(255, 255, 255, 0.6);
border-radius: 50%;
pointer-events: none;
transform: scale(0);
animation: ripple-animation 0.6s linear;
}
@keyframes ripple-animation {
to {
transform: scale(4);
opacity: 0;
}
}
`;
document.head.appendChild(style);
</script>
</body>
</html>

View File

@@ -0,0 +1,30 @@
{
"name": "Laca City - Smart Parking",
"short_name": "Laca City",
"description": "Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi.",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#E85A4F",
"orientation": "portrait-primary",
"icons": [
{
"src": "/favicon.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/favicon.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["navigation", "travel", "utilities"],
"lang": "en",
"dir": "ltr",
"scope": "/",
"related_applications": [],
"prefer_related_applications": false
}

View File

@@ -0,0 +1,14 @@
User-agent: *
Allow: /
# Sitemap
Sitemap: https://yourdomain.com/sitemap.xml
# Specific paths
Allow: /homepage/
Allow: /assets/
Allow: /?app=parking
# Disallow admin or private sections (if any)
# Disallow: /admin/
# Disallow: /private/

View File

@@ -2,20 +2,6 @@
@tailwind components;
@tailwind utilities;
/* Import Leaflet CSS */
@import 'leaflet/dist/leaflet.css';
/* Leaflet container fixes for Next.js and full-screen rendering */
.leaflet-container {
height: 100% !important;
width: 100% !important;
z-index: 1 !important;
}
.leaflet-control-container {
z-index: 1000 !important;
}
/* Full screen layout fixes */
html, body {
height: 100%;
@@ -28,146 +14,62 @@ html, body {
height: 100%;
}
/* Map container specific fixes */
.map-container {
height: 100% !important;
width: 100% !important;
min-height: 400px !important;
position: relative;
}
.map-container .leaflet-container {
height: 100% !important;
width: 100% !important;
min-height: inherit !important;
}
/* Ensure proper flex behavior for full-screen maps */
/* Ensure proper flex behavior for full-screen layouts */
.flex-1 {
min-height: 0;
min-width: 0;
}
/* Custom Map Marker Animations */
/* GPS Marker Animations */
@keyframes pulse-gps {
0% {
transform: scale(0.8);
opacity: 0.6;
}
50% {
transform: scale(1.2);
opacity: 0.2;
}
100% {
transform: scale(0.8);
opacity: 0.6;
}
/* Global custom variables */
:root {
--primary-color: #e85a4f;
--secondary-color: #d2001c;
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
}
@keyframes blink-gps {
0%, 50% {
opacity: 1;
}
51%, 100% {
opacity: 0.3;
}
/* Custom scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
/* Parking Marker Animations */
@keyframes pulse-parking {
0% {
transform: scale(1);
opacity: 0.8;
}
50% {
transform: scale(1.1);
opacity: 0.4;
}
100% {
transform: scale(1);
opacity: 0.8;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
/* Custom marker classes */
.gps-marker-icon,
.gps-marker-icon-enhanced {
background: transparent !important;
border: none !important;
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
/* Parking Finder Button Animations */
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-6px);
}
100% {
transform: translateY(0px);
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
@keyframes pulse-glow {
0% {
box-shadow: 0 10px 30px rgba(232, 90, 79, 0.4), 0 0 20px rgba(232, 90, 79, 0.3);
}
50% {
box-shadow: 0 15px 40px rgba(232, 90, 79, 0.6), 0 0 30px rgba(232, 90, 79, 0.5);
}
100% {
box-shadow: 0 10px 30px rgba(232, 90, 79, 0.4), 0 0 20px rgba(232, 90, 79, 0.3);
}
/* Loading spinner animation */
@keyframes spin {
to { transform: rotate(360deg); }
}
.parking-finder-button {
animation: float 3s ease-in-out infinite, pulse-glow 2s ease-in-out infinite;
.animate-spin {
animation: spin 1s linear infinite;
}
.parking-finder-button:hover {
animation: none;
/* Smooth transitions for better UX */
button, input, select, textarea, .interactive {
transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.parking-marker-icon,
.parking-marker-icon-enhanced {
background: transparent !important;
border: none !important;
}
/* Enhanced popup styles with animation */
.leaflet-popup-content-wrapper {
border-radius: 16px !important;
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.15),
0 10px 20px rgba(0, 0, 0, 0.1) !important;
border: 1px solid rgba(0, 0, 0, 0.05) !important;
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.95) !important;
animation: popup-appear 0.3s ease-out;
}
.leaflet-popup-content {
margin: 20px !important;
line-height: 1.6 !important;
font-size: 14px !important;
}
.leaflet-popup-tip {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
}
@keyframes popup-appear {
0% {
opacity: 0;
transform: scale(0.8) translateY(10px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
/* Focus styles for accessibility */
button:focus,
input:focus,
select:focus,
textarea:focus {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* Enhanced Filter Box Animations */
@@ -273,24 +175,29 @@ html, body {
}
}
/* Custom pulse animation for selected elements */
@keyframes selected-pulse {
0% {
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7);
/* Animation utilities */
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
70% {
box-shadow: 0 0 0 10px rgba(220, 38, 38, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0);
to {
opacity: 1;
transform: translateY(0);
}
}
/* Hover effects for markers */
.leaflet-marker-icon:hover {
z-index: 1000 !important;
filter: brightness(1.1) saturate(1.2);
transition: all 0.2s ease-in-out;
.animate-fade-in {
animation: fade-in 0.3s ease-out;
}
.animate-slide-up {
animation: slide-up 0.3s ease-out;
}
/* Enhanced animations for GPS simulator */
@@ -326,79 +233,11 @@ html, body {
}
}
.marker-loading {
.loading-animation {
animation: spin-slow 2s linear infinite;
}
/* Enhanced mobile responsiveness for markers */
@media (max-width: 768px) {
.leaflet-popup-content-wrapper {
max-width: 280px !important;
}
.leaflet-popup-content {
margin: 12px !important;
font-size: 14px !important;
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
.gps-marker-icon,
.parking-marker-icon {
filter: contrast(1.5) saturate(1.2);
}
}
/* Reduce motion for accessibility */
@media (prefers-reduced-motion: reduce) {
.gps-marker-icon *,
.parking-marker-icon * {
animation: none !important;
}
}
/* Fix for Leaflet attribution */
.leaflet-control-attribution {
font-size: 10px !important;
}
/* Custom marker styles */
.custom-div-icon {
background: none !important;
border: none !important;
}
.leaflet-pane {
z-index: 1;
}
.leaflet-control-zoom {
z-index: 2;
}
.leaflet-control-attribution {
z-index: 2;
}
/* Custom CSS Variables */
:root {
--primary-color: #E85A4F;
--secondary-color: #D73502;
--accent-color: #8B2635;
--success-color: #22C55E;
--warning-color: #F59E0B;
--danger-color: #EF4444;
--neutral-color: #6B7280;
}
/* Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
@@ -410,95 +249,26 @@ body {
background-color: #ffffff;
}
/* Custom Scrollbar */
/* Custom Scrollbar (unified) */
::-webkit-scrollbar {
width: 6px;
height: 6px;
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Leaflet Map Overrides */
.leaflet-container {
height: 100%;
width: 100%;
border-radius: 0.5rem;
}
.leaflet-popup-content-wrapper {
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.leaflet-popup-tip {
background: white;
}
.leaflet-control-zoom {
border-radius: 0.5rem !important;
border: none !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) !important;
}
.leaflet-control-zoom a {
border-radius: 0.25rem !important;
border: none !important;
background-color: white !important;
color: #374151 !important;
font-weight: 600;
transition: all 0.2s ease;
}
.leaflet-control-zoom a:hover {
background-color: #f3f4f6 !important;
color: var(--primary-color) !important;
}
/* Custom Map Marker Styles */
.parking-marker {
background: white;
border: 2px solid var(--primary-color);
border-radius: 50%;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: var(--primary-color);
transition: all 0.2s ease;
}
.parking-marker:hover {
transform: scale(1.1);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.parking-marker.available {
border-color: var(--success-color);
color: var(--success-color);
}
.parking-marker.limited {
border-color: var(--warning-color);
color: var(--warning-color);
}
.parking-marker.full {
border-color: var(--danger-color);
color: var(--danger-color);
}
/* Animation Classes */
@keyframes shimmer {
0% {

View File

@@ -0,0 +1,618 @@
'use client';
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
export default function Homepage() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [showScrollToTop, setShowScrollToTop] = useState(false);
const handleScrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId);
if (element) {
const headerOffset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
setMobileMenuOpen(false);
}
};
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
useEffect(() => {
const checkScrollTop = () => {
if (!showScrollToTop && window.pageYOffset > 400) {
setShowScrollToTop(true);
} else if (showScrollToTop && window.pageYOffset <= 400) {
setShowScrollToTop(false);
}
};
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element;
if (mobileMenuOpen && !target.closest('nav')) {
setMobileMenuOpen(false);
}
};
window.addEventListener('scroll', checkScrollTop);
document.addEventListener('mousedown', handleClickOutside);
return () => {
window.removeEventListener('scroll', checkScrollTop);
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showScrollToTop, mobileMenuOpen]);
return (
<div className="min-h-screen bg-white">
{/* Navigation */}
<nav className="bg-white shadow-lg border-b-4 sticky top-0 z-50" style={{ borderBottomColor: 'var(--primary-color)' }}>
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="flex items-center justify-between h-20">
<div className="flex items-center space-x-4">
<div className="relative">
<Image
src="/assets/Logo_and_sologan.png"
alt="Laca City Logo"
width={280}
height={70}
className="h-16 w-auto object-contain"
/>
</div>
</div>
<div className="hidden lg:flex space-x-10">
<button
onClick={() => handleScrollToSection('about')}
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
>
About Us
</button>
<button
onClick={() => handleScrollToSection('how-it-works')}
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
>
How It Works
</button>
<button
onClick={() => handleScrollToSection('community')}
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
>
Community
</button>
<button
onClick={() => handleScrollToSection('contact')}
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
>
Contact
</button>
</div>
{/* Mobile menu button */}
<div className="lg:hidden">
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="text-white bg-primary-500 p-3 rounded-xl hover:shadow-xl transition-all duration-300 font-bold"
>
MENU
</button>
</div>
<div className="hidden lg:flex space-x-4">
<button
onClick={() => window.location.href = '/?view=parking'}
className="bg-white text-gray-700 border-2 border-gray-300 hover:border-primary-500 hover:text-primary-600 px-6 py-3 rounded-xl font-medium transition-all duration-300 transform hover:scale-105 shadow-lg"
>
Open App
</button>
<button
onClick={() => window.location.href = '/?view=parking'}
className="text-white px-8 py-3 rounded-xl font-medium transition-all duration-300 transform hover:scale-105 shadow-xl"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 8px 25px rgba(232, 90, 79, 0.3)'
}}
>
Get Started
</button>
</div>
</div>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<div className="lg:hidden bg-white border-t-2 shadow-xl" style={{ borderTopColor: 'var(--primary-color)' }}>
<div className="px-6 py-6 space-y-3">
<button
onClick={() => handleScrollToSection('about')}
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
>
About Us
</button>
<button
onClick={() => handleScrollToSection('how-it-works')}
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
>
How It Works
</button>
<button
onClick={() => handleScrollToSection('community')}
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
>
Community
</button>
<button
onClick={() => handleScrollToSection('contact')}
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
>
Contact
</button>
<div className="pt-4 border-t-2 border-gray-200">
<button
onClick={() => window.location.href = '/?view=parking'}
className="block w-full text-white px-6 py-4 rounded-xl font-medium transition-all duration-300 transform hover:scale-105 shadow-xl"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 8px 25px rgba(232, 90, 79, 0.3)'
}}
>
Get Started
</button>
</div>
</div>
</div>
)}
</nav>
{/* Hero Section */}
<section className="relative overflow-hidden" style={{
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))'
}}>
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10 py-24 md:py-32">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<div className="text-center lg:text-left">
<h1 className="text-5xl md:text-6xl lg:text-7xl font-extrabold leading-tight mb-8" style={{ color: 'var(--primary-color)' }}>
Park Easy, Move Breezy
</h1>
<h2 className="text-2xl md:text-3xl text-gray-700 mb-10 leading-relaxed font-medium">
Find and share parking spots in seconds. Save time, fuel, and reduce stress in Ho Chi Minh City & Hanoi.
</h2>
<div className="flex flex-col sm:flex-row gap-6 justify-center lg:justify-start mb-10">
<button
onClick={() => window.location.href = '/?view=parking'}
className="text-white px-10 py-5 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
}}
>
Start Finding Parking
</button>
<button
onClick={() => window.location.href = '/?view=parking'}
className="bg-white text-gray-700 border-3 border-primary-500 hover:text-white hover:bg-primary-500 px-10 py-5 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
>
Share a Spot
</button>
</div>
<p className="text-gray-600 text-xl leading-relaxed font-normal">
Join thousands of drivers reimagining streets for people, making urban driving easy and sustainable.
</p>
</div>
<div className="relative">
<div className="bg-white rounded-3xl shadow-2xl p-8 transform rotate-2 hover:rotate-0 transition-transform duration-500 border-4" style={{ borderColor: 'var(--primary-color)' }}>
<Image
src="/assets/Location.png"
alt="Laca City App Interface"
width={600}
height={500}
className="w-full h-auto rounded-2xl"
/>
<div className="absolute -top-6 -right-6 text-white px-6 py-3 rounded-2xl text-lg font-semibold shadow-2xl" style={{ background: 'var(--primary-color)' }}>
Your city's parking assistant
</div>
</div>
</div>
</div>
</div>
</section>
{/* Problem & Story Section */}
<section className="py-24 bg-white">
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="text-center mb-20">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
Tired of Circling for Parking?
</h2>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<div>
<p className="text-xl text-gray-700 mb-8 leading-relaxed font-normal">
Urban drivers in Vietnam spend 1530 minutes per trip searching for parking—burning fuel, wasting time, and adding to congestion.
</p>
<p className="text-xl text-gray-700 mb-8 leading-relaxed font-normal">
When founder Mai Nguyen returned to Vietnam, she saw sidewalks turned into parking lots, forcing pedestrians into the street. Delivery drivers and gig workers spend hours searching for spots, day after day.
</p>
<p className="text-xl text-gray-700 leading-relaxed font-medium" style={{ color: 'var(--primary-color)' }}>
Laca City was born to end this struggle—making parking easy while reclaiming streets for people.
</p>
</div>
<div className="relative">
<div className="grid grid-cols-2 gap-6">
<div className="bg-red-50 border-4 rounded-2xl p-8 text-center transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<div className="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--primary-color)' }}>
<span className="text-white font-bold text-2xl">!</span>
</div>
<h4 className="font-semibold mb-3 text-xl" style={{ color: 'var(--primary-color)' }}>Before Laca City</h4>
<p className="font-medium" style={{ color: 'var(--primary-color)' }}>15-30 minutes circling, wasting fuel, stress</p>
</div>
<div className="bg-green-50 border-4 rounded-2xl p-8 text-center transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--secondary-color)', backgroundColor: 'rgba(210, 0, 28, 0.1)' }}>
<div className="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--secondary-color)' }}>
<span className="text-white font-bold text-2xl">✓</span>
</div>
<h4 className="font-semibold mb-3 text-xl" style={{ color: 'var(--secondary-color)' }}>With Laca City</h4>
<p className="font-medium" style={{ color: 'var(--secondary-color)' }}>Instant parking, happy drivers</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* How It Works Section */}
<section id="how-it-works" className="py-24 scroll-mt-20" style={{
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))'
}}>
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="text-center mb-20">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
Find. Share. Drive Happy.
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-10 mb-16">
{/* Step 1 */}
<div className="text-center bg-white rounded-3xl p-10 shadow-2xl hover:shadow-3xl transition-all duration-300 transform hover:scale-105 border-4" style={{ borderColor: 'var(--primary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
1
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-6" style={{ color: 'var(--primary-color)' }}>Find Parking Instantly</h3>
<p className="text-gray-700 font-normal text-lg">Open Laca City to see real-time parking spots near you.</p>
</div>
{/* Step 2 */}
<div className="text-center bg-white rounded-3xl p-10 shadow-2xl hover:shadow-3xl transition-all duration-300 transform hover:scale-105 border-4" style={{ borderColor: 'var(--primary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--primary-color)' }}>
2
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-6" style={{ color: 'var(--primary-color)' }}>Share a Spot</h3>
<p className="text-gray-700 font-normal text-lg">Add public, private, or peer-to-peer parking to help other drivers.</p>
</div>
{/* Step 3 */}
<div className="text-center bg-white rounded-3xl p-10 shadow-2xl hover:shadow-3xl transition-all duration-300 transform hover:scale-105 border-4" style={{ borderColor: 'var(--secondary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--secondary-color)' }}>
3
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-6" style={{ color: 'var(--secondary-color)' }}>Get Recognition</h3>
<p className="text-gray-700 font-normal text-lg">Earn badges and leaderboard positions for contributing to a smarter city.</p>
</div>
</div>
<div className="text-center">
<button
onClick={() => window.location.href = '/?view=parking'}
className="text-white px-12 py-6 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
}}
>
Start Finding Parking
</button>
</div>
</div>
</section>
{/* Community & Gamification Section */}
<section id="community" className="py-24 bg-white scroll-mt-20">
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="text-center mb-20">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
Built by Drivers, for Drivers
</h2>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<div>
<p className="text-xl text-gray-700 mb-10 leading-relaxed font-normal">
Your shared spots power the map. Laca City is entirely community-driven, rewarding contributors with:
</p>
<div className="space-y-8 mb-10">
<div className="flex items-center space-x-6 p-6 rounded-2xl border-4 transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<div className="flex-shrink-0 w-16 h-16 rounded-2xl flex items-center justify-center text-white font-bold text-2xl" style={{ background: 'var(--primary-color)' }}>
HERO
</div>
<div>
<h4 className="font-semibold text-gray-900 text-xl">Parking Hero badges</h4>
<p className="text-gray-700 font-normal">Get recognized for your contributions</p>
</div>
</div>
<div className="flex items-center space-x-6 p-6 rounded-2xl border-4 transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--secondary-color)', backgroundColor: 'rgba(210, 0, 28, 0.1)' }}>
<div className="flex-shrink-0 w-16 h-16 rounded-2xl flex items-center justify-center text-white font-bold text-2xl" style={{ background: 'var(--secondary-color)' }}>
RANK
</div>
<div>
<h4 className="font-semibold text-gray-900 text-xl">Weekly leaderboards</h4>
<p className="text-gray-700 font-normal">Compete with other contributors</p>
</div>
</div>
<div className="flex items-center space-x-6 p-6 rounded-2xl border-4 transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<div className="flex-shrink-0 w-16 h-16 rounded-2xl flex items-center justify-center text-white font-bold text-2xl" style={{ background: 'var(--primary-color)' }}>
FAME
</div>
<div>
<h4 className="font-semibold text-gray-900 text-xl">Social media shoutouts</h4>
<p className="text-gray-700 font-normal">Get featured for your community impact</p>
</div>
</div>
</div>
<p className="text-xl text-gray-700 mb-10 font-normal">
Help your city run better while getting recognized in the community.
</p>
<button
onClick={() => window.location.href = '/?view=parking'}
className="text-white px-10 py-5 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
}}
>
Share Your First Spot Today
</button>
</div>
<div className="rounded-3xl p-10 border-4 shadow-2xl" style={{
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))',
borderColor: 'var(--primary-color)'
}}>
<h4 className="text-2xl font-bold text-gray-900 mb-8" style={{ color: 'var(--primary-color)' }}>Weekly Leaderboard</h4>
<div className="space-y-6">
<div className="flex items-center justify-between p-6 border-4 rounded-2xl transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--primary-color)' }}>
<span className="text-white font-bold text-lg">1</span>
</div>
<div>
<p className="font-semibold text-gray-900 text-lg">Minh Nguyen</p>
<p className="text-gray-600 font-normal">District 1</p>
</div>
</div>
<span className="font-semibold text-xl" style={{ color: 'var(--primary-color)' }}>28 spots</span>
</div>
<div className="flex items-center justify-between p-6 border-4 rounded-2xl transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--secondary-color)', backgroundColor: 'rgba(210, 0, 28, 0.1)' }}>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--secondary-color)' }}>
<span className="text-white font-bold text-lg">2</span>
</div>
<div>
<p className="font-semibold text-gray-900 text-lg">Linh Tran</p>
<p className="text-gray-600 font-normal">District 3</p>
</div>
</div>
<span className="font-semibold text-xl" style={{ color: 'var(--secondary-color)' }}>22 spots</span>
</div>
<div className="flex items-center justify-between p-6 border-4 rounded-2xl transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--primary-color)' }}>
<span className="text-white font-bold text-lg">3</span>
</div>
<div>
<p className="font-semibold text-gray-900 text-lg">Duc Le</p>
<p className="text-gray-600 font-normal">District 7</p>
</div>
</div>
<span className="font-semibold text-xl" style={{ color: 'var(--primary-color)' }}>19 spots</span>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Join the Movement Section */}
<section className="py-24 text-white" style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
}}>
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10 text-center">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8">
Help Build Vietnam's First Real-Time Parking Map
</h2>
<p className="text-2xl mb-12 max-w-4xl mx-auto leading-relaxed font-medium">
Sign up free. Find parking in seconds. Share your spots. Together, we'll reclaim streets for people and make our cities smarter.
</p>
<div className="flex flex-col sm:flex-row gap-6 justify-center">
<button
onClick={() => window.location.href = '/?view=parking'}
className="bg-white px-12 py-6 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
style={{ color: 'var(--primary-color)' }}
>
Start Finding Parking
</button>
<button
onClick={() => window.location.href = '/?view=parking'}
className="bg-transparent border-4 border-white text-white hover:bg-white px-12 py-6 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl hover:text-red-500"
>
Start Sharing Spots
</button>
</div>
</div>
</section>
{/* About Us / Mission Section */}
<section id="about" className="py-24 bg-white scroll-mt-20">
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="text-center mb-20">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
Smart Parking for Smart Cities
</h2>
<p className="text-2xl text-gray-700 max-w-5xl mx-auto leading-relaxed font-medium">
Laca City connects drivers with real-time parking spots, reducing congestion and reclaiming sidewalks for pedestrians.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center mb-20">
<div>
<p className="text-xl text-gray-700 mb-8 leading-relaxed font-normal">
As Vietnam's cities prepare for autonomous vehicles and low-emission transport, we're building the digital parking infrastructure they need.
</p>
<p className="text-2xl font-bold leading-relaxed" style={{ color: 'var(--primary-color)' }}>
"Streets for people" - Đường phố cho con người.
</p>
</div>
<div className="rounded-3xl p-10 text-center border-4 shadow-2xl" style={{
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))',
borderColor: 'var(--primary-color)'
}}>
<div className="w-32 h-32 rounded-full mx-auto mb-8 overflow-hidden border-4" style={{ borderColor: 'var(--primary-color)' }}>
<div className="w-full h-full flex items-center justify-center text-white text-5xl font-semibold" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
MN
</div>
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-3">Mai Nguyen</h3>
<p className="font-semibold mb-6 text-xl" style={{ color: 'var(--primary-color)' }}>Founder & CEO</p>
<p className="text-gray-700 leading-relaxed font-normal">
Urban planner with global experience (World Bank, Asia & North America).
Passionate about creating cities where streets belong to people.
</p>
</div>
</div>
</div>
</section>
{/* Social Proof & Partnerships Section */}
<section className="py-24" style={{
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))'
}}>
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="text-center mb-20">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
Trusted by Community and Partners
</h2>
<p className="text-2xl text-gray-700 max-w-5xl mx-auto leading-relaxed font-medium">
We're working with universities, small businesses, and city pilot programs to make urban parking easy.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
<div className="bg-white rounded-3xl p-10 shadow-2xl text-center transform hover:scale-105 transition-transform duration-300 border-4" style={{ borderColor: 'var(--primary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--primary-color)' }}>
UN
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Vietnam National University</h3>
<p className="text-gray-700 font-normal">Student Parking Pilot</p>
</div>
<div className="bg-white rounded-3xl p-10 shadow-2xl text-center transform hover:scale-105 transition-transform duration-300 border-4" style={{ borderColor: 'var(--secondary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--secondary-color)' }}>
CF
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">District 1 Cafe Network</h3>
<p className="text-gray-700 font-normal">Private Spot Sharing</p>
</div>
<div className="bg-white rounded-3xl p-10 shadow-2xl text-center transform hover:scale-105 transition-transform duration-300 border-4" style={{ borderColor: 'var(--primary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--primary-color)' }}>
HN
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Hanoi Transportation Dept</h3>
<p className="text-gray-700 font-normal">Public Lot Integration</p>
</div>
</div>
</div>
</section>
{/* Footer */}
<footer id="contact" className="bg-gray-900 text-white py-20 scroll-mt-20">
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
<div className="md:col-span-2">
<div className="flex items-center space-x-4 mb-8">
<Image
src="/assets/Footer_page_logo.png"
alt="Laca City Logo"
width={60}
height={60}
className="h-15 w-15 object-contain"
/>
<span className="text-3xl font-black">Laca City</span>
</div>
<p className="text-gray-300 text-2xl mb-8 leading-relaxed font-bold">
Park Easy, Move Breezy.
</p>
<p className="text-gray-400 leading-relaxed font-normal text-lg">
Making urban parking easy while reclaiming streets for people.
</p>
</div>
<div>
<h4 className="text-xl font-semibold mb-8">Quick Links</h4>
<ul className="space-y-4">
<li><button onClick={() => handleScrollToSection('about')} className="text-gray-300 hover:text-white transition-colors cursor-pointer font-normal text-lg">About Us</button></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors font-normal text-lg">Blog</a></li>
<li><button onClick={() => handleScrollToSection('contact')} className="text-gray-300 hover:text-white transition-colors cursor-pointer font-normal text-lg">Contact</button></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors font-normal text-lg">Terms</a></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors font-normal text-lg">Privacy</a></li>
</ul>
</div>
<div>
<h4 className="text-xl font-semibold mb-8">Connect</h4>
<div className="flex space-x-6">
<a href="#" className="text-gray-400 hover:text-white transition-colors">
<span className="sr-only">Zalo</span>
<div className="w-12 h-12 rounded-2xl flex items-center justify-center font-semibold text-xl text-white" style={{ background: 'var(--primary-color)' }}>
Z
</div>
</a>
<a href="#" className="text-gray-400 hover:text-white transition-colors">
<span className="sr-only">Facebook</span>
<div className="w-12 h-12 rounded-2xl flex items-center justify-center font-semibold text-xl text-white" style={{ background: 'var(--secondary-color)' }}>
f
</div>
</a>
<a href="#" className="text-gray-400 hover:text-white transition-colors">
<span className="sr-only">TikTok</span>
<div className="w-12 h-12 rounded-2xl flex items-center justify-center font-semibold text-xl text-white" style={{ background: 'var(--primary-color)' }}>
T
</div>
</a>
</div>
</div>
</div>
<div className="border-t border-gray-800 pt-10 text-center">
<p className="text-gray-400 font-normal text-lg">
© 2025 Laca City. All rights reserved. Made with love for Vietnamese drivers.
</p>
</div>
</div>
</footer>
{/* Scroll to Top Button */}
{showScrollToTop && (
<button
onClick={scrollToTop}
className="fixed bottom-10 right-10 text-white p-4 rounded-2xl shadow-2xl transition-all duration-300 transform hover:scale-110 z-50 font-semibold text-lg"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
}}
aria-label="Scroll to top"
>
TOP
</button>
)}
</div>
);
}

View File

@@ -7,46 +7,44 @@ import { Toaster } from 'react-hot-toast';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: '',
description: '',
keywords: ['parking', 'navigation', 'maps', 'HCMC', 'Vietnam', 'bãi đỗ xe', 'TP.HCM'],
authors: [{ name: 'Smart Parking Team' }],
creator: 'Smart Parking Team',
publisher: 'Smart Parking HCMC',
title: 'Laca City - Park Easy, Move Breezy',
description: 'Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi. Join thousands of drivers reclaiming streets for people.',
keywords: ['parking', 'navigation', 'HCMC', 'Vietnam', 'bãi đỗ xe', 'TP.HCM', 'Hanoi', 'smart parking', 'Laca City'],
authors: [{ name: 'Laca City Team' }],
creator: 'Laca City',
publisher: 'Laca City',
robots: 'index, follow',
openGraph: {
type: 'website',
locale: 'vi_VN',
url: 'https://parking-hcmc.com',
title: '',
description: '',
siteName: 'Smart Parking HCMC',
url: 'https://lacacity.com',
title: 'Laca City - Park Easy, Move Breezy',
description: 'Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi.',
siteName: 'Laca City',
images: [
{
url: '/assets/Logo_and_sologan.png',
url: '/assets/Location.png',
width: 1200,
height: 630,
alt: 'Smart Parking HCMC',
alt: 'Laca City - Smart Parking Solution',
},
],
},
twitter: {
card: 'summary_large_image',
title: '',
description: '',
images: ['/assets/Logo_and_sologan.png'],
title: 'Laca City - Park Easy, Move Breezy',
description: 'Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi.',
images: ['/assets/Location.png'],
},
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
},
themeColor: '#2563EB',
themeColor: '#E85A4F',
manifest: '/manifest.json',
icons: {
icon: '/assets/mini_location.png',
shortcut: '/assets/mini_location.png',
apple: '/assets/Logo.png',
icon: '/favicon.png?v=5',
},
};

View File

@@ -8,23 +8,9 @@ import { HCMCGPSSimulator } from '@/components/HCMCGPSSimulator';
// import { ErrorMessage } from '@/components/ui/ErrorMessage';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { useParkingSearch } from '@/hooks/useParkingSearch';
import { useRouting } from '@/hooks/useRouting';
import { ParkingLot, UserLocation, TransportationMode } from '@/types';
import toast from 'react-hot-toast';
// Dynamic import for map component (client-side only)
const MapView = dynamic(
() => import('@/components/map/MapView').then((mod) => mod.MapView),
{
ssr: false,
loading: () => (
<div className="h-full flex items-center justify-center bg-gray-100 rounded-lg">
<LoadingSpinner size="lg" />
</div>
),
}
);
export default function ParkingFinderPage() {
// State management
const [selectedParkingLot, setSelectedParkingLot] = useState<ParkingLot | null>(null);
@@ -42,14 +28,6 @@ export default function ParkingFinderPage() {
searchLocation
} = useParkingSearch();
const {
route,
isLoading: routeLoading,
error: routeError,
calculateRoute,
clearRoute
} = useRouting();
// Handle GPS location change from simulator
const handleLocationChange = (location: UserLocation) => {
setUserLocation(location);
@@ -70,35 +48,16 @@ export default function ParkingFinderPage() {
}
};
const handleParkingLotSelect = async (lot: ParkingLot) => {
const handleParkingLotSelect = (lot: ParkingLot) => {
// If the same parking lot is selected again, deselect it
if (selectedParkingLot && selectedParkingLot.id === lot.id) {
setSelectedParkingLot(null);
clearRoute();
toast.success('Đã bỏ chọn bãi đỗ xe');
return;
}
setSelectedParkingLot(lot);
if (userLocation) {
try {
await calculateRoute(
{ latitude: userLocation.lat, longitude: userLocation.lng },
{ latitude: lot.lat, longitude: lot.lng },
{ mode: 'driving' }
);
toast.success(`Đã tính đường đến ${lot.name}`);
} catch (error) {
toast.error('Không thể tính toán đường đi');
}
}
};
const handleClearRoute = () => {
clearRoute();
setSelectedParkingLot(null);
toast.success('Đã xóa tuyến đường');
toast.success(`Đã chọn ${lot.name}`);
};
// Show error messages
@@ -108,35 +67,35 @@ export default function ParkingFinderPage() {
}
}, [parkingError]);
useEffect(() => {
if (routeError) {
toast.error(routeError);
}
}, [routeError]);
return (
<div className="min-h-screen bg-gray-50">
<Header
title="Smart Parking Finder - TP.HCM"
subtitle="Chỉ hỗ trợ ô tô"
onClearRoute={route ? handleClearRoute : undefined}
/>
<main className="container mx-auto px-4 py-6">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 h-full">
{/* Left Column - Map and Parking List */}
<div className="lg:col-span-3 space-y-6">
{/* Map Section */}
{/* Summary Section */}
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
<div className="h-96">
<MapView
userLocation={userLocation}
parkingLots={parkingLots}
selectedParkingLot={selectedParkingLot}
route={route}
onParkingLotSelect={handleParkingLotSelect}
isLoading={routeLoading}
/>
<div className="h-96 bg-gradient-to-br from-gray-50 to-blue-50 flex items-center justify-center">
<div className="text-center p-8">
<div className="w-24 h-24 bg-blue-100 rounded-full mx-auto mb-6 flex items-center justify-center">
<svg className="w-12 h-12 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-800 mb-3">Parking Finder - HCMC</h2>
<p className="text-gray-600 mb-4">Find and book parking spots in Ho Chi Minh City</p>
{parkingLots.length > 0 && (
<div className="text-sm text-gray-500">
Found {parkingLots.length} parking locations nearby
</div>
)}
</div>
</div>
</div>
@@ -199,14 +158,6 @@ export default function ParkingFinderPage() {
</div>
</div>
)}
{routeError && (
<div className="fixed bottom-4 right-4 max-w-sm">
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{routeError}
</div>
</div>
)}
</main>
</div>
);

View File

@@ -1,8 +1,9 @@
'use client';
import React, { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
import { useSearchParams } from 'next/navigation';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { ParkingList } from '@/components/parking/ParkingList';
import { ParkingDetails } from '@/components/parking/ParkingDetails';
import { HCMCGPSSimulator } from '@/components/HCMCGPSSimulator';
@@ -10,24 +11,42 @@ import { Icon } from '@/components/ui/Icon';
// import { ErrorMessage } from '@/components/ui/ErrorMessage';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { useParkingSearch } from '@/hooks/useParkingSearch';
import { useRouting } from '@/hooks/useRouting';
import { ParkingLot, UserLocation, TransportationMode } from '@/types';
import toast from 'react-hot-toast';
// Dynamic import for map component (client-side only) - NO loading component to prevent unnecessary loading states
const MapView = dynamic(
() => import('@/components/map/MapView').then((mod) => mod.MapView),
{
ssr: false,
loading: () => null, // Remove loading spinner to prevent map reload appearance
}
);
export default function MainPage() {
const searchParams = useSearchParams();
const showApp = searchParams?.get('app') === 'parking';
export default function ParkingFinderPage() {
if (showApp) {
return <ParkingFinderPage />;
}
// Show Canva homepage by default
return <CanvaHomepage />;
}
function CanvaHomepage() {
useEffect(() => {
// Redirect to the Canva homepage in the public directory
window.location.href = '/homepage/index.html';
}, []);
return (
<div className="h-screen bg-white flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-red-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600">Loading homepage...</p>
</div>
</div>
);
}
function ParkingFinderPage() {
// State management
const [selectedParkingLot, setSelectedParkingLot] = useState<ParkingLot | null>(null);
const [userLocation, setUserLocation] = useState<UserLocation | null>(null);
const [searchRadius, setSearchRadius] = useState(4000); // meters - bán kính 4km
const [searchRadius, setSearchRadius] = useState(4000); // meters - 4km radius
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
const [gpsWindowPos, setGpsWindowPos] = useState({ x: 0, y: 20 });
const [isMobile, setIsMobile] = useState(false);
@@ -88,14 +107,6 @@ export default function ParkingFinderPage() {
searchLocation
} = useParkingSearch();
const {
route,
isLoading: routeLoading,
error: routeError,
calculateRoute,
clearRoute
} = useRouting();
// Handle GPS location change from simulator
const handleLocationChange = (location: UserLocation) => {
setUserLocation(location);
@@ -103,16 +114,16 @@ export default function ParkingFinderPage() {
// Search for parking near the new location
if (location) {
searchLocation({ latitude: location.lat, longitude: location.lng });
toast.success('Đã cập nhật vị trí GPS và tìm kiếm bãi đỗ xe gần đó');
toast.success('GPS location updated and searched for nearby parking lots');
}
};
const handleRefresh = () => {
if (userLocation) {
searchLocation({ latitude: userLocation.lat, longitude: userLocation.lng });
toast.success('Đã làm mới danh sách bãi đỗ xe');
toast.success('Parking list refreshed');
} else {
toast.error('Vui lòng chọn vị trí GPS trước');
toast.error('Please select GPS location first');
}
};
@@ -120,25 +131,12 @@ export default function ParkingFinderPage() {
// Toggle selection
if (selectedParkingLot?.id === lot.id) {
setSelectedParkingLot(null);
clearRoute();
return;
}
setSelectedParkingLot(lot);
if (userLocation) {
try {
await calculateRoute(
{ latitude: userLocation.lat, longitude: userLocation.lng },
{ latitude: lot.lat, longitude: lot.lng },
{ mode: 'driving' }
);
toast.success(`Đã tính đường đến ${lot.name}`);
} catch (error) {
console.error('Error calculating route:', error);
toast.error('Không thể tính toán tuyến đường');
}
}
setLeftSidebarOpen(false); // Close sidebar when selecting parking lot
toast.success(`Selected ${lot.name}`);
};
const handleParkingLotViewing = (lot: ParkingLot | null) => {
@@ -146,9 +144,8 @@ export default function ParkingFinderPage() {
};
const handleClearRoute = () => {
clearRoute();
setSelectedParkingLot(null);
toast.success('Đã xóa tuyến đường');
toast.success('Selection cleared');
};
// Show error messages
@@ -158,18 +155,11 @@ export default function ParkingFinderPage() {
}
}, [parkingError]);
useEffect(() => {
if (routeError) {
toast.error(routeError);
}
}, [routeError]);
return (
<div className="h-screen bg-gray-50 flex flex-col">
<Header
title=""
subtitle=""
onClearRoute={route ? handleClearRoute : undefined}
/>
<main className="flex-1 flex relative bg-white">
@@ -206,9 +196,9 @@ export default function ParkingFinderPage() {
</div>
<div>
<h2 className="text-xl font-bold text-gray-900 tracking-tight">
Bãi đ xe gần đây
Nearby Parking Lots
</h2>
<p className="text-sm text-gray-600 font-medium">Tìm kiếm thông minh</p>
<p className="text-sm text-gray-600 font-medium">Smart Search</p>
</div>
</div>
<div className="flex items-center space-x-2">
@@ -229,7 +219,7 @@ export default function ParkingFinderPage() {
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Làm mới danh sách
Refresh List
</button>
{/* Status Info Bar - Thiết kế thanh lịch đơn giản */}
@@ -241,14 +231,14 @@ export default function ParkingFinderPage() {
<div className="flex items-center space-x-2">
<div className="w-2 h-2 rounded-full bg-emerald-500"></div>
<span className="text-sm text-gray-700 font-medium">
{parkingLots.filter(lot => lot.availableSlots > 0).length} chỗ
{parkingLots.filter(lot => lot.availableSlots > 0).length} available
</span>
</div>
<div className="w-px h-4 bg-gray-300"></div>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 rounded-full bg-red-500"></div>
<span className="text-sm text-gray-700 font-medium">
{parkingLots.filter(lot => lot.availableSlots === 0).length} đy
{parkingLots.filter(lot => lot.availableSlots === 0).length} full
</span>
</div>
</div>
@@ -275,7 +265,7 @@ export default function ParkingFinderPage() {
<div className="relative">
<input
type="text"
placeholder="Tìm kiếm bãi đỗ xe..."
placeholder="Search parking lots..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-4 py-3 pl-12 pr-10 text-sm font-medium rounded-2xl border-2 transition-all duration-300 focus:outline-none focus:ring-4 focus:ring-orange-100 focus:border-orange-300"
@@ -320,7 +310,7 @@ export default function ParkingFinderPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707v4.586a1 1 0 01-.54.89l-2 1A1 1 0 0110 20v-5.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
</div>
<span className="text-lg font-bold" style={{ color: 'var(--accent-color)' }}>Sắp xếp:</span>
<span className="text-lg font-bold" style={{ color: 'var(--accent-color)' }}>Sort:</span>
</div>
<div className="flex gap-2">
@@ -339,7 +329,7 @@ export default function ParkingFinderPage() {
borderColor: sortType === 'availability' ? 'var(--primary-color)' : 'rgba(232, 90, 79, 0.3)',
border: '2px solid'
}}
title="Sắp xếp theo chỗ trống"
title="Sort by availability"
>
<Icon
name="car"
@@ -363,7 +353,7 @@ export default function ParkingFinderPage() {
borderColor: sortType === 'price' ? '#10B981' : 'rgba(16, 185, 129, 0.3)',
border: '2px solid'
}}
title="Sắp xếp theo giá rẻ"
title="Sort by price"
>
<Icon
name="currency"
@@ -394,7 +384,7 @@ export default function ParkingFinderPage() {
: userLocation ? 'rgba(245, 158, 11, 0.3)' : '#E5E7EB',
border: '2px solid'
}}
title="Sắp xếp theo khoảng cách gần nhất"
title="Sort by nearest distance"
>
<Icon
name="distance"
@@ -419,8 +409,8 @@ export default function ParkingFinderPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">Chọn vị trí GPS</h3>
<p className="text-gray-600 text-sm">Vui lòng chọn vị trí GPS đ tìm bãi đ xe gần đó</p>
<h3 className="text-lg font-bold text-gray-900 mb-2">Select GPS Location</h3>
<p className="text-gray-600 text-sm">Please select a GPS location to find nearby parking lots</p>
</div>
) : parkingLots.length === 0 ? (
<div className="text-center py-12">
@@ -429,8 +419,8 @@ export default function ParkingFinderPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.732 15c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">Không bãi đ xe</h3>
<p className="text-gray-600 text-sm">Không tìm thấy bãi đ xe nào gần vị trí này</p>
<h3 className="text-lg font-bold text-gray-900 mb-2">No Parking Lots</h3>
<p className="text-gray-600 text-sm">No parking lots found near this location</p>
</div>
) : (
<ParkingList
@@ -468,89 +458,34 @@ export default function ParkingFinderPage() {
userLocation={userLocation}
onClose={() => {
setSelectedParkingLot(null);
clearRoute();
}}
onBook={(lot) => {
toast.success(`Đã đặt chỗ tại ${lot.name}!`);
toast.success(`Booked parking at ${lot.name}!`);
// Here you would typically call an API to book the parking spot
}}
/>
</div>
)}
{/* Map Section - Right */}
{/* Right Section - Summary Information */}
<div className="flex-1 h-full relative">
<MapView
userLocation={userLocation}
parkingLots={parkingLots}
selectedParkingLot={selectedParkingLot}
route={route}
onParkingLotSelect={handleParkingLotSelect}
className="w-full h-full"
/>
{/* Map overlay info - Position based on layout */}
{userLocation && (
<div className="absolute bottom-6 right-24 z-10 bg-white rounded-3xl shadow-2xl p-6 border-2 border-gray-100 backdrop-blur-sm" style={{ minWidth: '280px' }}>
<div className="flex items-center space-x-4 mb-4">
<div className="w-12 h-12 rounded-2xl flex items-center justify-center shadow-lg" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
<img
src="/assets/Logo.png"
alt="Logo"
className="w-7 h-7 object-contain filter brightness-0 invert"
/>
</div>
<div>
<h3 className="text-lg font-bold text-gray-900 tracking-tight">Parking Finder</h3>
<p className="text-sm text-gray-600 font-medium">Bản đ thông minh</p>
</div>
<div className="w-full h-full bg-gradient-to-br from-gray-50 to-blue-50 rounded-2xl flex items-center justify-center border border-gray-200">
<div className="text-center p-8">
<div className="w-24 h-24 bg-blue-100 rounded-full mx-auto mb-6 flex items-center justify-center">
<svg className="w-12 h-12 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div className="space-y-2">
{/* Current location */}
<div className="flex items-center space-x-3 p-2 rounded-xl bg-blue-50">
<div className="w-4 h-4 rounded-full shadow-sm" style={{ backgroundColor: '#3B82F6' }}></div>
<span className="text-sm font-semibold text-blue-800">Vị trí hiện tại</span>
<h2 className="text-2xl font-bold text-gray-800 mb-3">Map in Developing</h2>
<p className="text-gray-600 mb-4">Interactive map feature coming soon</p>
{parkingLots.length > 0 && (
<div className="text-sm text-gray-500">
Found {parkingLots.length} parking locations nearby
</div>
{/* Parking lot status legend */}
<div className="bg-gray-50 rounded-xl p-3 space-y-2">
<div className="text-xs font-bold text-gray-700 mb-2">Trạng thái bãi xe:</div>
{/* Available parking - Green */}
<div className="flex items-center space-x-3 p-1">
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: 'var(--success-color)' }}></div>
<span className="text-xs font-medium text-green-700">Còn chỗ thoáng (&gt;70%)</span>
</div>
{/* Nearly full - Yellow */}
<div className="flex items-center space-x-3 p-1">
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: '#F59E0B' }}></div>
<span className="text-xs font-medium text-yellow-700">Sắp đy (&lt;30%)</span>
</div>
{/* Full - Red */}
<div className="flex items-center space-x-3 p-1">
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: '#EF4444' }}></div>
<span className="text-xs font-medium text-red-700">Hết chỗ</span>
</div>
{/* Closed - Gray */}
<div className="flex items-center space-x-3 p-1">
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: '#6B7280' }}></div>
<span className="text-xs font-medium text-gray-700">Đã đóng cửa</span>
</div>
</div>
{/* Route line */}
{route && (
<div className="flex items-center space-x-3 p-2 rounded-xl bg-red-50">
<div className="w-4 h-2 rounded-full shadow-sm" style={{ backgroundColor: 'var(--primary-color)' }}></div>
<span className="text-sm font-semibold" style={{ color: 'var(--primary-color)' }}>Tuyến đưng</span>
</div>
)}
</div>
)}
</div>
)}
</div>
</div>
{/* Floating GPS Window */}
@@ -595,14 +530,14 @@ export default function ParkingFinderPage() {
<p className="text-white text-opacity-90 font-medium" style={{
fontSize: isMobile ? '12px' : '14px'
}}>
{isMobile ? 'Mô phỏng GPS' : 'Mô phỏng vị trí GPS cho TP.HCM'}
{isMobile ? 'GPS Simulation' : 'GPS Location Simulation for Ho Chi Minh City'}
</p>
</div>
{isMobile && (
<button
onClick={() => setGpsSimulatorVisible(!gpsSimulatorVisible)}
className="p-2 rounded-xl bg-white bg-opacity-20 hover:bg-opacity-30 transition-all duration-200"
title={gpsSimulatorVisible ? 'Ẩn GPS Simulator' : 'Hiện GPS Simulator'}
title={gpsSimulatorVisible ? 'Hide GPS Simulator' : 'Show GPS Simulator'}
>
<svg
className={`w-4 h-4 text-white transition-transform duration-300 ${gpsSimulatorVisible ? 'rotate-180' : 'rotate-0'}`}
@@ -620,7 +555,7 @@ export default function ParkingFinderPage() {
<button
onClick={() => setGpsSimulatorVisible(!gpsSimulatorVisible)}
className="p-2 rounded-xl bg-white bg-opacity-20 hover:bg-opacity-30 transition-all duration-200 group"
title={gpsSimulatorVisible ? 'Ẩn GPS Simulator' : 'Hiện GPS Simulator'}
title={gpsSimulatorVisible ? 'Hide GPS Simulator' : 'Show GPS Simulator'}
>
<svg
className={`w-5 h-5 text-white transition-transform duration-300 ${gpsSimulatorVisible ? 'rotate-180' : 'rotate-0'}`}
@@ -654,6 +589,9 @@ export default function ParkingFinderPage() {
</div>
</main>
{/* Footer */}
<Footer showFullFooter={false} />
{/* Show errors */}
{parkingError && (
<div className="fixed bottom-6 right-6 max-w-sm z-50">
@@ -662,14 +600,6 @@ export default function ParkingFinderPage() {
</div>
</div>
)}
{routeError && (
<div className="fixed bottom-6 right-6 max-w-sm z-50">
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{routeError}
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,113 @@
'use client';
import React from 'react';
import Image from 'next/image';
interface FooterProps {
showFullFooter?: boolean;
className?: string;
}
export const Footer: React.FC<FooterProps> = ({
showFullFooter = false,
className = ""
}) => {
if (!showFullFooter) {
return (
<footer className={`bg-black text-white py-6 ${className}`}>
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="flex items-center justify-center">
<div className="flex items-center space-x-4">
<Image
src="/assets/Footer_page_logo.png"
alt="Laca City Logo"
width={40}
height={40}
className="h-10 w-auto object-contain"
/>
<span className="text-lg font-bold">Laca City</span>
</div>
</div>
</div>
</footer>
);
}
return (
<footer className={`bg-black text-white py-16 ${className}`}>
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-12">
<div className="md:col-span-2">
<div className="flex items-center space-x-4 mb-6">
<Image
src="/assets/Footer_page_logo.png"
alt="Laca City Logo"
width={60}
height={60}
className="h-16 w-auto object-contain"
/>
<span className="text-2xl font-bold">Laca City</span>
</div>
<p className="text-xl text-gray-200 leading-relaxed mb-6 max-w-lg">
Revolutionizing urban parking with smart technology, real-time data, and user-friendly solutions for Ho Chi Minh City.
</p>
{/* Social Media Links */}
<div className="flex space-x-4">
<a href="#" className="group bg-gray-800 hover:bg-red-500 p-3 rounded-xl transition-all duration-300 transform hover:scale-110">
{/* Facebook Icon */}
<svg className="w-6 h-6 text-gray-300 group-hover:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
</a>
<a href="#" className="group bg-gray-800 hover:bg-red-500 p-3 rounded-xl transition-all duration-300 transform hover:scale-110">
{/* Twitter Icon */}
<svg className="w-6 h-6 text-gray-300 group-hover:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
</svg>
</a>
<a href="#" className="group bg-gray-800 hover:bg-red-500 p-3 rounded-xl transition-all duration-300 transform hover:scale-110">
{/* LinkedIn Icon */}
<svg className="w-6 h-6 text-gray-300 group-hover:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
</div>
</div>
<div>
<h4 className="text-lg font-semibold mb-6">Quick Links</h4>
<ul className="space-y-3">
<li><a href="/" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Home</a></li>
<li><a href="/#features" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Features</a></li>
<li><a href="/#team" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Team</a></li>
<li><a href="/#news" className="text-gray-300 hover:text-red-500 transition-colors duration-300">News</a></li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold mb-6">App</h4>
<ul className="space-y-3">
<li><a href="/?app=parking" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Launch App</a></li>
<li><a href="#" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Help</a></li>
<li><a href="#" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Privacy</a></li>
<li><a href="#" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Terms</a></li>
</ul>
</div>
</div>
{/* Copyright */}
<div className="border-t border-gray-800 pt-8">
<div className="flex flex-col md:flex-row items-center justify-between">
<p className="text-gray-400 text-sm">
© 2024 Laca City. All rights reserved.
</p>
<p className="text-gray-400 text-sm mt-2 md:mt-0">
Made with in Ho Chi Minh City
</p>
</div>
</div>
</div>
</footer>
);
};

View File

@@ -10,31 +10,31 @@ interface HCMCGPSSimulatorProps {
// Predefined locations near HCMC parking lots
const simulationPoints = [
// Trung tâm Quận 1 - gần bãi đỗ xe
// District 1 Center - near parking lots
{
name: 'Vincom Center Đồng Khởi',
location: { lat: 10.7769, lng: 106.7009 },
description: 'Gần trung tâm thương mại Vincom'
description: 'Near Vincom shopping center'
},
{
name: 'Saigon Centre',
location: { lat: 10.7743, lng: 106.7017 },
description: 'Gần Saigon Centre'
description: 'Near Saigon Centre'
},
{
name: 'Landmark 81',
location: { lat: 10.7955, lng: 106.7195 },
description: 'Gần tòa nhà Landmark 81'
description: 'Near Landmark 81 building'
},
{
name: 'Bitexco Financial Tower',
location: { lat: 10.7718, lng: 106.7047 },
description: 'Gần tòa nhà Bitexco'
description: 'Near Bitexco building'
},
{
name: 'Chợ Bến Thành',
location: { lat: 10.7729, lng: 106.6980 },
description: 'Gần chợ Bến Thành'
description: 'Near Ben Thanh Market'
},
{
name: 'Diamond Plaza',
@@ -240,7 +240,7 @@ export const HCMCGPSSimulator: React.FC<HCMCGPSSimulatorProps> = ({
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 md:gap-3 mb-1">
<span className="text-base md:text-lg font-bold tracking-tight" style={{ color: 'var(--primary-color)' }}>Vị trí hiện tại</span>
<span className="text-base md:text-lg font-bold tracking-tight" style={{ color: 'var(--primary-color)' }}>Current Location</span>
<div className="flex items-center gap-1 md:gap-2 px-2 md:px-3 py-1 rounded-full bg-white border-2" style={{ borderColor: 'var(--success-color)' }}>
<div className="w-1.5 md:w-2 h-1.5 md:h-2 rounded-full animate-pulse" style={{ backgroundColor: 'var(--success-color)' }}></div>
<span className="text-xs font-bold" style={{ color: 'var(--success-color)' }}>LIVE</span>
@@ -483,8 +483,8 @@ export const HCMCGPSSimulator: React.FC<HCMCGPSSimulatorProps> = ({
</svg>
</div>
<div className="text-left flex-1 min-w-0">
<h5 className="text-base md:text-lg font-bold tracking-tight mb-1" style={{ color: 'var(--accent-color)' }}>Vị trí ngẫu nhiên</h5>
<p className="text-xs md:text-sm text-gray-600 font-medium">Tạo tọa đ tự đng trong TP.HCM</p>
<h5 className="text-base md:text-lg font-bold tracking-tight mb-1" style={{ color: 'var(--accent-color)' }}>Random Location</h5>
<p className="text-xs md:text-sm text-gray-600 font-medium">Auto-generate coordinates in HCMC</p>
<div className="flex items-center gap-2 mt-2">
<div className="flex items-center gap-1 px-2 py-1 rounded-full" style={{ backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" style={{ color: 'var(--primary-color)' }}>
@@ -493,7 +493,7 @@ export const HCMCGPSSimulator: React.FC<HCMCGPSSimulatorProps> = ({
<span className="text-xs font-bold" style={{ color: 'var(--primary-color)' }}>RANDOM</span>
</div>
<div className="w-1 h-1 rounded-full" style={{ backgroundColor: 'var(--primary-color)' }}></div>
<span className="text-xs text-gray-500 hidden md:inline">Khu vực mở rộng</span>
<span className="text-xs text-gray-500 hidden md:inline">Extended area</span>
</div>
</div>
<div className="w-5 md:w-6 h-5 md:h-6 rounded-full border-2 flex items-center justify-center group-hover:border-red-500 transition-colors flex-shrink-0" style={{ borderColor: 'var(--primary-color)' }}>

View File

@@ -26,18 +26,12 @@ export const Header: React.FC<HeaderProps> = ({
<div className="flex-shrink-0">
<div className="relative">
<Image
src="/assets/Logo_and_sologan.png"
src="/assets/Location.png"
alt="Smart Parking Logo"
width={320}
height={80}
className="h-18 w-auto object-contain"
/>
{/* Animated accent line */}
<div className="absolute bottom-0 left-0 right-0 h-1 rounded-full" style={{
background: 'linear-gradient(90deg, var(--primary-color), var(--secondary-color))',
transform: 'scaleX(0.8)',
transformOrigin: 'left'
}}></div>
</div>
</div>
)}
@@ -66,7 +60,7 @@ export const Header: React.FC<HeaderProps> = ({
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
</svg>
Xóa tuyến đưng
Clear Route
</button>
)}
@@ -76,7 +70,7 @@ export const Header: React.FC<HeaderProps> = ({
borderColor: 'rgba(34, 197, 94, 0.3)'
}}>
<div className="w-3 h-3 rounded-full animate-pulse shadow-sm" style={{ backgroundColor: 'var(--success-color)' }}></div>
<span className="text-sm font-bold" style={{ color: 'var(--success-color)' }}>Dữ liệu trực tuyến</span>
<span className="text-sm font-bold" style={{ color: 'var(--success-color)' }}>Live Data</span>
</div>
{/* City Info */}
@@ -90,7 +84,7 @@ export const Header: React.FC<HeaderProps> = ({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<span className="text-sm font-bold" style={{ color: 'var(--primary-color)' }}>TP. Hồ Chí Minh</span>
<span className="text-sm font-bold" style={{ color: 'var(--primary-color)' }}>Ho Chi Minh City</span>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@ interface ParkingFloor {
walkways: { x: number; y: number; width: number; height: number }[];
}
// Thiết kế bãi xe đẹp và chuyên nghiệp
// Professional and beautiful parking lot design
const generateParkingFloorData = (floorNumber: number): ParkingFloor => {
const slots: ParkingSlot[] = [];
const walkways = [];
@@ -108,7 +108,7 @@ const generateParkingFloorData = (floorNumber: number): ParkingFloor => {
return {
floor: floorNumber,
name: `Tầng ${floorNumber}`,
name: `Floor ${floorNumber}`,
slots,
entrances: [
{ x: 60, y: 10, type: 'entrance' },
@@ -194,7 +194,7 @@ const ParkingLotMap: React.FC<{ parkingLot: ParkingLot }> = ({ parkingLot }) =>
{/* Real-time indicator */}
<div className="flex items-center gap-2 text-xs text-gray-500">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span>Cập nhật: {lastUpdate.toLocaleTimeString()}</span>
<span>Updated: {lastUpdate.toLocaleTimeString()}</span>
</div>
</div>
@@ -202,15 +202,15 @@ const ParkingLotMap: React.FC<{ parkingLot: ParkingLot }> = ({ parkingLot }) =>
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="text-center p-2 bg-green-50 rounded border border-green-200">
<div className="font-bold text-green-600">{floorStats.available}</div>
<div className="text-green-700">Trống</div>
<div className="text-green-700">Available</div>
</div>
<div className="text-center p-2 bg-red-50 rounded border border-red-200">
<div className="font-bold text-red-600">{floorStats.occupied}</div>
<div className="text-red-700">Đã đu</div>
<div className="text-red-700">Occupied</div>
</div>
<div className="text-center p-2 bg-gray-50 rounded border border-gray-200">
<div className="font-bold text-gray-600">{floorStats.total}</div>
<div className="text-gray-700">Tổng</div>
<div className="text-gray-700">Total</div>
</div>
</div>
</div>
@@ -221,27 +221,27 @@ const ParkingLotMap: React.FC<{ parkingLot: ParkingLot }> = ({ parkingLot }) =>
const SAMPLE_REVIEWS = [
{
id: 1,
user: 'Nguyễn Văn A',
user: 'John Smith',
rating: 5,
comment: 'Bãi xe rộng rãi, bảo vệ 24/7 rất an toàn. Giá cả hợp lý.',
comment: 'Spacious parking lot with 24/7 security. Very safe and reasonably priced.',
date: '2024-01-15',
avatar: 'N'
avatar: 'J'
},
{
id: 2,
user: 'Trần Thị B',
user: 'Sarah Johnson',
rating: 4,
comment: 'Vị trí thuận tin, dễ tìm. Chỉ hơi xa lối ra một chút.',
comment: 'Convenient location, easy to find. Just a bit far from the exit.',
date: '2024-01-10',
avatar: 'T'
avatar: 'S'
},
{
id: 3,
user: 'Lê Văn C',
user: 'Mike Davis',
rating: 5,
comment: 'Có sạc điện cho xe điện, rất tiện lợi!',
comment: 'Has electric charging stations, very convenient!',
date: '2024-01-08',
avatar: 'L'
avatar: 'M'
}
];
@@ -326,8 +326,8 @@ const formatAmenities = (amenities: string[] | { [key: string]: any }): string[]
const amenityList: string[] = [];
if (amenities.covered) amenityList.push('Có mái che');
if (amenities.security) amenityList.push('Bảo vệ 24/7');
if (amenities.ev_charging) amenityList.push('Sạc xe điện');
if (amenities.security) amenityList.push('24/7 Security');
if (amenities.ev_charging) amenityList.push('EV Charging');
if (amenities.wheelchair_accessible) amenityList.push('Phù hợp xe lăn');
if (amenities.valet_service) amenityList.push('Dịch vụ đỗ xe');
@@ -418,7 +418,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
{renderStars(Math.round(averageRating))}
</div>
<span className="text-sm font-semibold">{averageRating.toFixed(1)}</span>
<span className="text-sm opacity-80">({SAMPLE_REVIEWS.length} đánh giá)</span>
<span className="text-sm opacity-80">({SAMPLE_REVIEWS.length} reviews)</span>
</div>
</div>
</div>
@@ -426,13 +426,13 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
{/* Status banners */}
{isFull && (
<div className="absolute bottom-0 left-0 right-0 bg-red-600 text-center py-2">
<span className="text-sm font-bold">Bãi xe đã hết chỗ</span>
<span className="text-sm font-bold">Parking lot is full</span>
</div>
)}
{isClosed && (
<div className="absolute bottom-0 left-0 right-0 bg-gray-600 text-center py-2">
<span className="text-sm font-bold">Bãi xe đã đóng cửa</span>
<span className="text-sm font-bold">Parking lot is closed</span>
</div>
)}
</div>
@@ -451,7 +451,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
<div className="text-2xl font-bold mb-1" style={{ color: statusColors.textColor }}>
{parkingLot.availableSlots}
</div>
<div className="text-xs font-medium text-gray-600">chỗ trống</div>
<div className="text-xs font-medium text-gray-600">available</div>
<div className="text-xs text-gray-500">/ {parkingLot.totalSlots} tổng</div>
</div>
@@ -505,7 +505,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
}}
>
{tab === 'overview' && 'Tổng quan'}
{tab === 'reviews' && 'Đánh giá'}
{tab === 'reviews' && 'Reviews'}
</button>
))}
</div>
@@ -541,7 +541,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
</div>
Tiện ích
Amenities
</h3>
<div className="grid grid-cols-1 gap-2">
{amenityList.map((amenity, index) => (
@@ -570,7 +570,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
<div className="flex items-center justify-center gap-1 mb-2">
{renderStars(Math.round(averageRating))}
</div>
<div className="text-sm font-medium text-gray-600">{SAMPLE_REVIEWS.length} đánh giá</div>
<div className="text-sm font-medium text-gray-600">{SAMPLE_REVIEWS.length} reviews</div>
</div>
{/* Reviews list */}
@@ -605,7 +605,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
<button className="w-full py-3 rounded-xl font-bold text-white shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105" style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
}}>
Viết đánh giá
Write Review
</button>
</div>
)}
@@ -722,7 +722,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
}}
>
{isFull ? 'Bãi xe đã hết chỗ' : isClosed ? 'Bãi xe đã đóng cửa' : `Đặt chỗ (${bookingDuration}h)`}
{isFull ? 'Parking lot is full' : isClosed ? 'Parking lot is closed' : `Book Spot (${bookingDuration}h)`}
</button>
</div>
)}

View File

@@ -43,21 +43,21 @@ const formatDistance = (distance: number): string => {
const getStatusColor = (availableSlots: number, totalSlots: number) => {
const percentage = availableSlots / totalSlots;
if (availableSlots === 0) {
// Hết chỗ - màu đỏ
// Full - red color
return {
background: 'rgba(239, 68, 68, 0.15)',
borderColor: '#EF4444',
textColor: '#EF4444'
};
} else if (percentage > 0.7) {
// >70% chỗ trống - màu xanh lá cây
// >70% available - green color
return {
background: 'rgba(34, 197, 94, 0.1)',
borderColor: 'var(--success-color)',
textColor: 'var(--success-color)'
};
} else {
// <30% chỗ trống - màu vàng
// <30% available - yellow color
return {
background: 'rgba(251, 191, 36, 0.1)',
borderColor: '#F59E0B',
@@ -68,11 +68,11 @@ const getStatusColor = (availableSlots: number, totalSlots: number) => {
const getStatusText = (availableSlots: number, totalSlots: number) => {
if (availableSlots === 0) {
return 'Hết chỗ';
return 'Full';
} else if (availableSlots / totalSlots > 0.7) {
return `${availableSlots} chỗ trống`;
return `${availableSlots} available`;
} else {
return `${availableSlots} chỗ trống (sắp hết)`;
return `${availableSlots} left (filling up)`;
}
};
@@ -201,9 +201,9 @@ export const ParkingList: React.FC<ParkingListProps> = ({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">Không tìm thấy kết quả</h3>
<p className="text-gray-600 text-sm">Không bãi đ xe nào phù hợp với từ khóa "{searchQuery}"</p>
<p className="text-gray-500 text-xs mt-2">Thử tìm kiếm với từ khóa khác</p>
<h3 className="text-lg font-bold text-gray-900 mb-2">No Results Found</h3>
<p className="text-gray-600 text-sm">No parking lots match the keyword "{searchQuery}"</p>
<p className="text-gray-500 text-xs mt-2">Try searching with different keywords</p>
</div>
) : (
sortedLots.map((lot, index) => {
@@ -262,13 +262,13 @@ export const ParkingList: React.FC<ParkingListProps> = ({
{/* Warning banners */}
{isFull && (
<div className="absolute -top-2 -left-2 -right-2 bg-red-500 text-white text-center py-2 rounded-t-xl shadow-lg z-20">
<span className="text-sm font-bold">🚫 BÃI XE ĐÃ HẾT CHỖ</span>
<span className="text-sm font-bold">🚫 PARKING LOT FULL</span>
</div>
)}
{isClosed && (
<div className="absolute -top-2 -left-2 -right-2 bg-gray-500 text-white text-center py-2 rounded-t-xl shadow-lg z-20">
<span className="text-sm font-bold">🔒 BÃI XE ĐÃ ĐÓNG CỬA</span>
<span className="text-sm font-bold">🔒 PARKING LOT CLOSED</span>
</div>
)}
@@ -322,10 +322,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
</div>
</div>
<div className="text-sm text-gray-500 font-medium">
chỗ trống
available
</div>
<div className="text-xs text-gray-400">
/ {lot.totalSlots} chỗ
/ {lot.totalSlots} total
</div>
{/* Availability percentage */}
<div className="mt-1 w-full bg-gray-200 rounded-full h-1.5 overflow-hidden">
@@ -338,7 +338,7 @@ export const ParkingList: React.FC<ParkingListProps> = ({
></div>
</div>
<div className="text-xs mt-1" style={{ color: statusColors.textColor }}>
{Math.round((lot.availableSlots / lot.totalSlots) * 100)}% trống
{Math.round((lot.availableSlots / lot.totalSlots) * 100)}% available
</div>
</div>
@@ -350,10 +350,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
{Math.round((lot.pricePerHour || lot.hourlyRate) / 1000)}k
</div>
<div className="text-sm text-gray-500 font-medium">
mỗi giờ
per hour
</div>
<div className="text-xs text-gray-400">
p gửi xe
parking fee
</div>
</>
) : (
@@ -362,10 +362,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
--
</div>
<div className="text-xs text-gray-400 font-medium">
liên hệ
contact
</div>
<div className="text-xs text-gray-400">
đ biết giá
for pricing
</div>
</>
)}
@@ -386,13 +386,13 @@ export const ParkingList: React.FC<ParkingListProps> = ({
</div>
<div className={`text-sm font-medium ${isCurrentlyOpen(lot) ? 'text-green-600' : 'text-red-600'}`}>
{isCurrentlyOpen(lot) ? (
lot.isOpen24Hours ? 'Luôn mở cửa' : `đến ${lot.closeTime}`
lot.isOpen24Hours ? 'Always open' : `until ${lot.closeTime}`
) : (
'Đã đóng cửa'
'Closed'
)}
</div>
<div className="text-xs text-gray-400">
{isCurrentlyOpen(lot) ? 'Đang mở' : '🔒 Đã đóng'}
{isCurrentlyOpen(lot) ? 'Open now' : '🔒 Closed'}
</div>
</>
) : (
@@ -401,10 +401,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
--:--
</div>
<div className="text-xs text-gray-400 font-medium">
không
unknown
</div>
<div className="text-xs text-gray-400">
giờ mở cửa
opening hours
</div>
</>
)}

View File

@@ -20,7 +20,8 @@ const iconPaths: Record<string, string> = {
delete: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16",
dice: "M5 3a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2H5zm3 4a1 1 0 100 2 1 1 0 000-2zm8 0a1 1 0 100 2 1 1 0 000-2zm-8 8a1 1 0 100 2 1 1 0 000-2zm8 0a1 1 0 100 2 1 1 0 000-2z",
location: "M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z",
map: "M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7v13zM9 7l6 2-6 3zm6-3l4.553 2.276A1 1 0 0121 7.618v10.764a1 1 0 01-.553.894L15 17V4z",
// map icon removed
marker: "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z",
market: "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2 2z M8 7V5a2 2 0 012-2h4a2 2 0 012 2v2",
refresh: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15",
rocket: "M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z",

View File

@@ -1,8 +1,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { parkingService, routingService, healthService } from '@/services/api';
import { parkingService, healthService } from '@/services/api';
import {
FindNearbyParkingRequest,
RouteRequest,
UpdateAvailabilityRequest
} from '@/types';
@@ -14,10 +13,6 @@ export const QUERY_KEYS = {
byId: (id: number) => ['parking', id],
popular: (limit?: number) => ['parking', 'popular', limit],
},
routing: {
route: (params: RouteRequest) => ['routing', 'route', params],
status: ['routing', 'status'],
},
health: ['health'],
} as const;
@@ -83,26 +78,6 @@ export function useUpdateParkingAvailability() {
});
}
// Routing hooks
export function useRoute(request: RouteRequest, enabled = true) {
return useQuery({
queryKey: QUERY_KEYS.routing.route(request),
queryFn: () => routingService.calculateRoute(request),
enabled: enabled && !!request.originLat && !!request.originLng && !!request.destinationLat && !!request.destinationLng,
staleTime: 15 * 60 * 1000, // 15 minutes
refetchOnWindowFocus: false,
});
}
export function useRoutingStatus() {
return useQuery({
queryKey: QUERY_KEYS.routing.status,
queryFn: routingService.getStatus,
staleTime: 30 * 1000, // 30 seconds
refetchInterval: 60 * 1000, // Refresh every minute
});
}
// Health hooks
export function useHealth() {
return useQuery({

View File

@@ -1,138 +0,0 @@
import { useState, useCallback } from 'react';
import { Coordinates } from '@/types';
export interface RouteStep {
instruction: string;
distance: number;
duration: number;
maneuver?: string;
}
export interface Route {
id: string;
distance: number; // in meters
duration: number; // in seconds
geometry: Array<[number, number]>; // [lat, lng] coordinates
steps: RouteStep[];
mode: 'driving' | 'walking' | 'cycling';
}
interface RoutingState {
route: Route | null;
alternatives: Route[];
isLoading: boolean;
error: string | null;
}
interface CalculateRouteOptions {
mode: 'driving' | 'walking' | 'cycling';
avoidTolls?: boolean;
avoidHighways?: boolean;
alternatives?: boolean;
}
export const useRouting = () => {
const [state, setState] = useState<RoutingState>({
route: null,
alternatives: [],
isLoading: false,
error: null
});
const calculateRoute = useCallback(async (
start: Coordinates,
end: Coordinates,
options: CalculateRouteOptions = { mode: 'driving' }
) => {
setState(prev => ({
...prev,
isLoading: true,
error: null
}));
try {
// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 1500));
// Mock route calculation
const distance = calculateDistance(start, end);
const mockRoute: Route = {
id: 'route-1',
distance: distance * 1000, // Convert to meters
duration: Math.round(distance * 180), // Rough estimate: 3 minutes per km for driving
geometry: [
[start.latitude, start.longitude],
[end.latitude, end.longitude]
],
steps: [
{
instruction: `Đi từ vị trí hiện tại`,
distance: distance * 1000 * 0.1,
duration: Math.round(distance * 18)
},
{
instruction: `Đến ${end.latitude.toFixed(4)}, ${end.longitude.toFixed(4)}`,
distance: distance * 1000 * 0.9,
duration: Math.round(distance * 162)
}
],
mode: options.mode
};
setState(prev => ({
...prev,
isLoading: false,
route: mockRoute,
alternatives: []
}));
return { route: mockRoute, alternatives: [] };
} catch (error: any) {
setState(prev => ({
...prev,
isLoading: false,
error: error.message || 'Failed to calculate route'
}));
throw error;
}
}, []);
const clearRoute = useCallback(() => {
setState({
route: null,
alternatives: [],
isLoading: false,
error: null
});
}, []);
return {
route: state.route,
alternatives: state.alternatives,
isLoading: state.isLoading,
error: state.error,
calculateRoute,
clearRoute
};
};
// Helper function to calculate distance between two coordinates
function calculateDistance(coord1: Coordinates, coord2: Coordinates): number {
const R = 6371; // Earth's radius in kilometers
const dLat = toRadians(coord2.latitude - coord1.latitude);
const dLon = toRadians(coord2.longitude - coord1.longitude);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRadians(coord1.latitude)) *
Math.cos(toRadians(coord2.latitude)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in kilometers
}
function toRadians(degrees: number): number {
return degrees * (Math.PI / 180);
}

View File

@@ -3,9 +3,7 @@ import {
FindNearbyParkingRequest,
FindNearbyParkingResponse,
ParkingLot,
UpdateAvailabilityRequest,
RouteRequest,
RouteResponse
UpdateAvailabilityRequest
} from '@/types';
class APIClient {
@@ -77,17 +75,6 @@ class APIClient {
return response.data;
}
// Routing endpoints
async calculateRoute(request: RouteRequest): Promise<RouteResponse> {
const response = await this.client.post('/routing/calculate', request);
return response.data;
}
async getRoutingServiceStatus(): Promise<{ status: string; version?: string }> {
const response = await this.client.get('/routing/status');
return response.data;
}
// Health endpoint
async getHealth(): Promise<{ status: string; timestamp: string }> {
const response = await this.client.get('/health');
@@ -108,11 +95,6 @@ export const parkingService = {
getPopular: (limit?: number) => apiClient.getPopularParkingLots(limit),
};
export const routingService = {
calculateRoute: (request: RouteRequest) => apiClient.calculateRoute(request),
getStatus: () => apiClient.getRoutingServiceStatus(),
};
export const healthService = {
getHealth: () => apiClient.getHealth(),
};

View File

@@ -54,37 +54,6 @@ export interface ParkingLot {
isOpen?: boolean;
}
export interface RoutePoint {
lat: number;
lng: number;
}
export interface RouteStep {
instruction: string;
distance: number; // meters
time: number; // seconds
type: string;
geometry: RoutePoint[];
}
export interface Route {
summary: {
distance: number; // km
time: number; // minutes
cost?: number; // estimated cost
};
geometry: RoutePoint[];
steps: RouteStep[];
confidence: number;
}
export interface RouteResponse {
routes: Route[];
origin: RoutePoint;
destination: RoutePoint;
requestId: string;
}
// API Request/Response Types
export interface FindNearbyParkingRequest {
lat: number;
@@ -102,17 +71,6 @@ export interface FindNearbyParkingResponse {
searchRadius: number;
}
export interface RouteRequest {
originLat: number;
originLng: number;
destinationLat: number;
destinationLng: number;
costing?: TransportationMode;
alternatives?: number;
avoidHighways?: boolean;
avoidTolls?: boolean;
}
export interface UpdateAvailabilityRequest {
availableSlots: number;
source?: string;
@@ -174,16 +132,8 @@ export interface ParkingState {
error: string | null;
}
export interface RouteState {
currentRoute: Route | null;
isCalculating: boolean;
error: string | null;
history: Route[];
}
export interface AppState {
parking: ParkingState;
routing: RouteState;
userPreferences: UserPreferences;
ui: {
sidebarOpen: boolean;
@@ -214,11 +164,6 @@ export interface ParkingLotSelectEvent {
source: 'map' | 'list' | 'search';
}
export interface RouteCalculatedEvent {
route: Route;
duration: number; // calculation time in ms
}
export interface LocationUpdateEvent {
location: UserLocation;
accuracy: number;
@@ -269,15 +214,6 @@ export interface SearchAnalytics {
timeToSelection?: number;
}
export interface RouteAnalytics {
origin: RoutePoint;
destination: RoutePoint;
mode: TransportationMode;
distance: number;
duration: number;
completed: boolean;
}
// Configuration Types
export interface AppConfig {
api: {
@@ -320,19 +256,9 @@ export interface UseParkingSearchReturn {
loadMore: () => void;
}
export interface UseRoutingReturn {
route: Route | null;
isLoading: boolean;
error: APIError | null;
calculateRoute: (request: RouteRequest) => Promise<void>;
clearRoute: () => void;
alternatives: Route[];
}
// Component Props Types
export interface HeaderProps {
onRefresh?: () => void;
onClearRoute?: () => void;
isLoading?: boolean;
}
@@ -340,7 +266,6 @@ export interface MapViewProps {
userLocation: UserLocation | null;
parkingLots: ParkingLot[];
selectedParkingLot: ParkingLot | null;
route: Route | null;
onParkingLotSelect: (lot: ParkingLot) => void;
isLoading?: boolean;
}

View File

@@ -1,194 +0,0 @@
import L from 'leaflet';
// Fix for default markers in React Leaflet
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
});
export interface MapBounds {
north: number;
south: number;
east: number;
west: number;
}
export interface MapUtils {
createIcon: (type: 'user' | 'parking' | 'selected') => L.Icon;
createBounds: (coordinates: Array<{ lat: number; lng: number }>) => L.LatLngBounds;
formatDistance: (distanceKm: number) => string;
formatDuration: (durationSeconds: number) => string;
getBoundsFromCoordinates: (coords: Array<[number, number]>) => MapBounds;
}
// Custom icons for different marker types
export const mapIcons = {
user: new L.Icon({
iconUrl: '/icons/location.svg',
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
className: 'user-location-icon',
}),
parking: new L.Icon({
iconUrl: '/icons/car.svg',
iconSize: [28, 28],
iconAnchor: [14, 28],
popupAnchor: [0, -28],
className: 'parking-icon',
}),
selected: new L.Icon({
iconUrl: '/icons/target.svg',
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
className: 'selected-parking-icon',
}),
unavailable: new L.Icon({
iconUrl: '/icons/warning.svg',
iconSize: [28, 28],
iconAnchor: [14, 28],
popupAnchor: [0, -28],
className: 'unavailable-parking-icon',
}),
};
// Map configuration constants
export const MAP_CONFIG = {
defaultCenter: { lat: 1.3521, lng: 103.8198 }, // Singapore
defaultZoom: 12,
maxZoom: 18,
minZoom: 10,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
tileLayerUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
searchRadius: 5000, // 5km in meters
};
// Utility functions
export const mapUtils: MapUtils = {
createIcon: (type: 'user' | 'parking' | 'selected') => {
return mapIcons[type];
},
createBounds: (coordinates: Array<{ lat: number; lng: number }>) => {
if (coordinates.length === 0) {
return new L.LatLngBounds(
[MAP_CONFIG.defaultCenter.lat, MAP_CONFIG.defaultCenter.lng],
[MAP_CONFIG.defaultCenter.lat, MAP_CONFIG.defaultCenter.lng]
);
}
const latLngs = coordinates.map(coord => new L.LatLng(coord.lat, coord.lng));
return new L.LatLngBounds(latLngs);
},
formatDistance: (distanceKm: number): string => {
if (distanceKm < 1) {
return `${Math.round(distanceKm * 1000)}m`;
}
return `${distanceKm.toFixed(1)}km`;
},
formatDuration: (durationSeconds: number): string => {
const minutes = Math.round(durationSeconds / 60);
if (minutes < 60) {
return `${minutes} min`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
},
getBoundsFromCoordinates: (coords: Array<[number, number]>): MapBounds => {
if (coords.length === 0) {
return {
north: MAP_CONFIG.defaultCenter.lat + 0.01,
south: MAP_CONFIG.defaultCenter.lat - 0.01,
east: MAP_CONFIG.defaultCenter.lng + 0.01,
west: MAP_CONFIG.defaultCenter.lng - 0.01,
};
}
const lats = coords.map(coord => coord[0]);
const lngs = coords.map(coord => coord[1]);
return {
north: Math.max(...lats),
south: Math.min(...lats),
east: Math.max(...lngs),
west: Math.min(...lngs),
};
},
};
// Route styling
export const routeStyle = {
color: '#2563eb', // Blue
weight: 4,
opacity: 0.8,
dashArray: '0',
lineJoin: 'round' as const,
lineCap: 'round' as const,
};
export const alternativeRouteStyle = {
color: '#6b7280', // Gray
weight: 3,
opacity: 0.6,
dashArray: '5, 10',
lineJoin: 'round' as const,
lineCap: 'round' as const,
};
// Parking lot status colors
export const parkingStatusColors = {
available: '#10b981', // Green
limited: '#f59e0b', // Amber
full: '#ef4444', // Red
unknown: '#6b7280', // Gray
};
// Helper function to get parking lot color based on availability
export const getParkingStatusColor = (
availableSpaces: number,
totalSpaces: number
): string => {
if (totalSpaces === 0) return parkingStatusColors.unknown;
const occupancyRate = 1 - (availableSpaces / totalSpaces);
if (occupancyRate < 0.7) return parkingStatusColors.available;
if (occupancyRate < 0.9) return parkingStatusColors.limited;
return parkingStatusColors.full;
};
// Animation utilities
export const animateMarker = (marker: L.Marker, newPosition: L.LatLng, duration = 1000) => {
const startPosition = marker.getLatLng();
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const currentLat = startPosition.lat + (newPosition.lat - startPosition.lat) * progress;
const currentLng = startPosition.lng + (newPosition.lng - startPosition.lng) * progress;
marker.setLatLng([currentLat, currentLng]);
if (progress < 1) {
requestAnimationFrame(animate);
}
};
animate();
};
// Bounds padding for better map view
export const boundsOptions = {
padding: [20, 20] as [number, number],
maxZoom: 16,
};