first commit
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
16
README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# React + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||||
29
eslint.config.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>tours-and-travels</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3457
package-lock.json
generated
Normal file
31
package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "tours-and-travels",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@react-oauth/google": "^0.13.4",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.4",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"autoprefixer": "^10.4.27",
|
||||||
|
"eslint": "^9.39.4",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
|
"globals": "^17.4.0",
|
||||||
|
"postcss": "^8.5.8",
|
||||||
|
"tailwindcss": "^3.4.19",
|
||||||
|
"vite": "^8.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
1
public/favicon.svg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
24
public/icons.svg
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||||
|
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||||
|
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||||
|
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
1
src/App.css
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/* App.css is no longer needed - all styles are handled by Tailwind via index.css */
|
||||||
42
src/App.jsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import Navbar from "./components/Navbar/Navbar";
|
||||||
|
import HeroSection from "./components/Hero/HeroSection";
|
||||||
|
import CabsSection from "./components/Cabs/CabsSection";
|
||||||
|
import ServicesSection from "./components/Services/ServicesSection";
|
||||||
|
import PromoBanners from "./components/Promotions/PromoBanners";
|
||||||
|
import ReviewsSection from "./components/Reviews/ReviewsSection";
|
||||||
|
import WhyChooseUs from "./components/Highlights/WhyChooseUs";
|
||||||
|
import NewsSection from "./components/News/NewsSection";
|
||||||
|
import Footer from "./components/Footer/Footer";
|
||||||
|
import AuthModal from "./components/Auth/AuthModal";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen">
|
||||||
|
<Navbar
|
||||||
|
user={user}
|
||||||
|
onLoginClick={() => setIsAuthModalOpen(true)}
|
||||||
|
/>
|
||||||
|
<HeroSection />
|
||||||
|
<CabsSection />
|
||||||
|
<ServicesSection />
|
||||||
|
<PromoBanners />
|
||||||
|
<ReviewsSection />
|
||||||
|
<WhyChooseUs />
|
||||||
|
<NewsSection />
|
||||||
|
<Footer />
|
||||||
|
<AuthModal
|
||||||
|
isOpen={isAuthModalOpen}
|
||||||
|
onClose={() => setIsAuthModalOpen(false)}
|
||||||
|
user={user}
|
||||||
|
setUser={setUser}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
BIN
src/assets/hero.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src/assets/hero_bg.png
Normal file
|
After Width: | Height: | Size: 819 KiB |
BIN
src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 285 KiB |
1
src/assets/react.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4 KiB |
BIN
src/assets/vehicles/audi_a6.png
Normal file
|
After Width: | Height: | Size: 621 KiB |
BIN
src/assets/vehicles/audi_q7.png
Normal file
|
After Width: | Height: | Size: 610 KiB |
BIN
src/assets/vehicles/bmw_5_series.png
Normal file
|
After Width: | Height: | Size: 557 KiB |
BIN
src/assets/vehicles/bmw_7_series.png
Normal file
|
After Width: | Height: | Size: 596 KiB |
BIN
src/assets/vehicles/camry.png
Normal file
|
After Width: | Height: | Size: 521 KiB |
BIN
src/assets/vehicles/corolla_altis.png
Normal file
|
After Width: | Height: | Size: 525 KiB |
BIN
src/assets/vehicles/dzire.png
Normal file
|
After Width: | Height: | Size: 575 KiB |
BIN
src/assets/vehicles/etios.png
Normal file
|
After Width: | Height: | Size: 538 KiB |
BIN
src/assets/vehicles/fortuner.png
Normal file
|
After Width: | Height: | Size: 566 KiB |
BIN
src/assets/vehicles/innova.png
Normal file
|
After Width: | Height: | Size: 539 KiB |
BIN
src/assets/vehicles/innova_crysta.png
Normal file
|
After Width: | Height: | Size: 604 KiB |
BIN
src/assets/vehicles/mercedes_e_class.png
Normal file
|
After Width: | Height: | Size: 575 KiB |
BIN
src/assets/vehicles/mercedes_s_class.png
Normal file
|
After Width: | Height: | Size: 640 KiB |
BIN
src/assets/vehicles/toyota_commuter.png
Normal file
|
After Width: | Height: | Size: 582 KiB |
1
src/assets/vite.svg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
108
src/components/Auth/AuthModal.jsx
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import PhoneInputScreen from "./PhoneInputScreen";
|
||||||
|
import OtpVerificationScreen from "./OtpVerificationScreen";
|
||||||
|
import UserProfileScreen from "./UserProfileScreen";
|
||||||
|
|
||||||
|
const AuthModal = ({ isOpen, onClose, user, setUser }) => {
|
||||||
|
const [view, setView] = useState("phone");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setView(user ? "profile" : "phone");
|
||||||
|
}
|
||||||
|
}, [isOpen, user]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm px-4">
|
||||||
|
{/* Click outside to close */}
|
||||||
|
<div className="absolute inset-0" onClick={onClose} />
|
||||||
|
|
||||||
|
{/* Modal Container */}
|
||||||
|
<div
|
||||||
|
className={`relative w-full shadow-2xl transition-all duration-300 ${
|
||||||
|
view === "phone" ? "max-w-4xl bg-transparent" : "max-w-md bg-white rounded-2xl"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Close button X */}
|
||||||
|
{(view === "otp" || view === "profile") && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 z-10 text-gray-500 hover:text-dark-900 transition-colors"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{view === "phone" && (
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 z-10 text-gray-500 hover:text-dark-900 transition-colors"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content View Switching */}
|
||||||
|
{view === "phone" && (
|
||||||
|
<PhoneInputScreen
|
||||||
|
onRequestOtp={() => setView("otp")}
|
||||||
|
onCancel={onClose}
|
||||||
|
onGoogleSuccess={async (tokenResponse) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://www.googleapis.com/oauth2/v1/userinfo?access_token=${tokenResponse.access_token}`);
|
||||||
|
const data = await res.json();
|
||||||
|
setUser(data);
|
||||||
|
setView("profile");
|
||||||
|
} catch (err) {
|
||||||
|
alert("Failed to fetch Google profile.");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRegisterSuccess={(formData) => {
|
||||||
|
// Creating a mock user from the native registration form
|
||||||
|
const newUser = {
|
||||||
|
name: formData.name,
|
||||||
|
email: formData.email,
|
||||||
|
picture: `https://ui-avatars.com/api/?name=${formData.name.replace(' ', '+')}&background=random`,
|
||||||
|
isNewRegistration: true
|
||||||
|
};
|
||||||
|
setUser(newUser);
|
||||||
|
setView("profile");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{view === "otp" && (
|
||||||
|
<OtpVerificationScreen
|
||||||
|
onBack={() => setView("phone")}
|
||||||
|
onSuccess={() => {
|
||||||
|
// Create a dummy user for the OTP flow demo
|
||||||
|
const fakeUser = {
|
||||||
|
name: "Customer",
|
||||||
|
email: "customer@phone.login",
|
||||||
|
picture: "https://ui-avatars.com/api/?name=Customer&background=random"
|
||||||
|
};
|
||||||
|
setUser(fakeUser);
|
||||||
|
setView("profile");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{view === "profile" && (
|
||||||
|
<UserProfileScreen
|
||||||
|
user={user}
|
||||||
|
onLogout={() => {
|
||||||
|
setUser(null);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthModal;
|
||||||
86
src/components/Auth/OtpVerificationScreen.jsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
|
||||||
|
const OtpVerificationScreen = ({ onBack, onSuccess }) => {
|
||||||
|
const [otp, setOtp] = useState(["", "", "", "", "", ""]);
|
||||||
|
const [timeLeft, setTimeLeft] = useState(20);
|
||||||
|
const inputRefs = useRef([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timeLeft > 0) {
|
||||||
|
const timer = setTimeout(() => setTimeLeft(timeLeft - 1), 1000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [timeLeft]);
|
||||||
|
|
||||||
|
const handleChange = (index, value) => {
|
||||||
|
if (isNaN(value)) return;
|
||||||
|
|
||||||
|
const newOtp = [...otp];
|
||||||
|
newOtp[index] = value;
|
||||||
|
setOtp(newOtp);
|
||||||
|
|
||||||
|
// Auto-focus next input
|
||||||
|
if (value !== "" && index < 5) {
|
||||||
|
inputRefs.current[index + 1].focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (index, e) => {
|
||||||
|
// Auto-focus previous input on backspace if current is empty
|
||||||
|
if (e.key === "Backspace" && index > 0 && otp[index] === "") {
|
||||||
|
inputRefs.current[index - 1].focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-sm mx-auto p-4 sm:p-8 animate-fadeInUp">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h2 className="text-2xl font-bold text-dark-900 mb-2">Verify Your Number</h2>
|
||||||
|
<p className="text-sm text-dark-700 leading-relaxed">
|
||||||
|
We've sent a 6-digit code to your phone. Please enter it below to continue.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 justify-center mb-6">
|
||||||
|
{otp.map((data, index) => (
|
||||||
|
<input
|
||||||
|
key={index}
|
||||||
|
type="text"
|
||||||
|
maxLength={1}
|
||||||
|
ref={(el) => (inputRefs.current[index] = el)}
|
||||||
|
value={data}
|
||||||
|
onChange={(e) => handleChange(index, e.target.value)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(index, e)}
|
||||||
|
className="w-10 h-10 sm:w-12 sm:h-12 border border-gray-300 rounded-lg text-center text-lg font-bold text-dark-900 focus:border-primary-600 focus:ring-1 focus:ring-primary-600 outline-none transition-all"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onSuccess}
|
||||||
|
className="w-full bg-primary-600 hover:bg-primary-700 text-white font-bold text-sm py-3.5 rounded-2xl mb-4 transition-colors"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
{timeLeft > 0 ? (
|
||||||
|
<p className="text-sm text-dark-400">Resend OTP in {timeLeft}s</p>
|
||||||
|
) : (
|
||||||
|
<button className="text-sm text-primary-600 font-semibold hover:text-primary-700">
|
||||||
|
Resend OTP
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center text-sm font-semibold text-primary-600 mt-2">
|
||||||
|
<button onClick={onBack} className="flex items-center gap-1 hover:text-primary-700">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||||
|
Edit number
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OtpVerificationScreen;
|
||||||
232
src/components/Auth/PhoneInputScreen.jsx
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useGoogleLogin } from "@react-oauth/google";
|
||||||
|
import { api } from "../../lib/api";
|
||||||
|
|
||||||
|
const PhoneInputScreen = ({ onRequestOtp, onCancel, onGoogleSuccess, onRegisterSuccess }) => {
|
||||||
|
const [activeTab, setActiveTab] = useState("login");
|
||||||
|
|
||||||
|
// Register Form State
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [phone, setPhone] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const [isRegistering, setIsRegistering] = useState(false);
|
||||||
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
|
|
||||||
|
const login = useGoogleLogin({
|
||||||
|
onSuccess: onGoogleSuccess,
|
||||||
|
onError: () => alert("Login Failed")
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
if (!name || !email || !phone || !password) {
|
||||||
|
setErrorMsg("Please fill all fields.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRegistering(true);
|
||||||
|
setErrorMsg("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Connects cleanly with the api configuration defined by VITE_API_URL or defaulted to local proxy.
|
||||||
|
const response = await api.post("/api/auth/register", {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pass the successful name/email down the line dynamically
|
||||||
|
onRegisterSuccess({ name: response.user?.name || name, email: response.user?.email || email, phone: response.user?.phone || phone });
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message.includes("409") || err.message.toLowerCase().includes("conflict") || err.message.toLowerCase().includes("already registered")) {
|
||||||
|
setErrorMsg("Email already registered. Did you mean to log in?");
|
||||||
|
} else {
|
||||||
|
setErrorMsg(err.message || "Registration failed. Please try again.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsRegistering(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col md:flex-row w-full h-full sm:h-[450px]">
|
||||||
|
{/* Left Promo Column */}
|
||||||
|
<div className="hidden md:flex flex-col md:w-5/12 relative rounded-l-2xl overflow-hidden bg-dark-900">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1549317661-bd32c8ce0db2?auto=format&fit=crop&q=80&w=800"
|
||||||
|
alt="Steering wheel"
|
||||||
|
className="absolute inset-0 w-full h-full object-cover opacity-60 mix-blend-overlay"
|
||||||
|
/>
|
||||||
|
<div className="relative z-10 flex flex-col justify-end h-full p-8 pb-10 bg-gradient-to-t from-black/80 to-transparent">
|
||||||
|
<h2 className="text-4xl font-extrabold text-white mb-2 leading-tight">10% OFF<br />
|
||||||
|
<span className="text-xl font-bold">on your first booking!</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-300 mb-6">
|
||||||
|
Use the coupon below to get flat 10% OFF on your first booking with road.rentals.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between border border-white/20 rounded-xl px-4 py-3 bg-black/40 backdrop-blur-md">
|
||||||
|
<span className="text-white font-mono font-bold tracking-wider">RE1ST10</span>
|
||||||
|
<button className="text-white hover:text-primary-400 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Auth Column */}
|
||||||
|
<div className="flex-1 p-6 sm:p-10 flex flex-col justify-center bg-white rounded-r-2xl animate-fadeInUp overflow-y-auto">
|
||||||
|
|
||||||
|
{/* Tab Switcher */}
|
||||||
|
<div className="flex bg-gray-100 rounded-xl p-1 mb-8 w-full max-w-[280px]">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("login")}
|
||||||
|
className={`flex-1 text-sm font-bold py-2 rounded-lg transition-all ${activeTab === 'login' ? 'bg-white text-dark-900 shadow-sm' : 'text-gray-500 hover:text-dark-700'}`}
|
||||||
|
>
|
||||||
|
Log In
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("register")}
|
||||||
|
className={`flex-1 text-sm font-bold py-2 rounded-lg transition-all ${activeTab === 'register' ? 'bg-white text-dark-900 shadow-sm' : 'text-gray-500 hover:text-dark-700'}`}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === "login" ? (
|
||||||
|
<>
|
||||||
|
<h2 className="text-2xl font-bold text-dark-900 mb-2">Welcome Back</h2>
|
||||||
|
<p className="text-sm text-dark-500 mb-8 leading-relaxed">
|
||||||
|
Sign in to book your rides, manage trips, and enjoy a seamless travel experience
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Phone Input Box */}
|
||||||
|
<div className="flex rounded-xl border border-gray-300 focus-within:border-primary-600 focus-within:ring-1 focus-within:ring-primary-600 transition-all mb-6">
|
||||||
|
<div className="flex flex-shrink-0 items-center gap-1.5 px-4 bg-gray-50 border-r border-gray-300 rounded-l-xl">
|
||||||
|
<span className="text-xl">🇮🇳</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-dark-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6" /></svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
placeholder="Phone number"
|
||||||
|
className="flex-1 w-full outline-none px-4 py-3.5 text-sm font-semibold rounded-r-xl placeholder:font-normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buttons Row */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-6">
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="w-full bg-gray-50 border border-gray-200 hover:bg-gray-100 text-dark-900 font-bold text-sm py-3.5 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onRequestOtp}
|
||||||
|
className="w-full bg-primary-600 hover:bg-primary-700 text-white font-bold text-sm py-3.5 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
Request OTP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2 className="text-2xl font-bold text-dark-900 mb-2">Create an Account</h2>
|
||||||
|
<p className="text-sm text-dark-500 mb-6 leading-relaxed">
|
||||||
|
Register now to start your reliable travel journey with road.rentals
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Register Fields */}
|
||||||
|
<div className="space-y-4 mb-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Full Name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full rounded-xl border border-gray-300 focus:border-primary-600 focus:ring-1 focus:ring-primary-600 outline-none px-4 py-3.5 text-sm transition-all"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email Address"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full rounded-xl border border-gray-300 focus:border-primary-600 focus:ring-1 focus:ring-primary-600 outline-none px-4 py-3.5 text-sm transition-all"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex rounded-xl border border-gray-300 focus-within:border-primary-600 focus-within:ring-1 focus-within:ring-primary-600 transition-all">
|
||||||
|
<div className="flex flex-shrink-0 items-center gap-1.5 px-4 bg-gray-50 border-r border-gray-300 rounded-l-xl">
|
||||||
|
<span className="text-xl">🇮🇳</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-dark-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6" /></svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
placeholder="Mobile Number"
|
||||||
|
value={phone}
|
||||||
|
onChange={(e) => setPhone(e.target.value)}
|
||||||
|
className="flex-1 w-full outline-none px-4 py-3.5 text-sm font-semibold rounded-r-xl placeholder:font-normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Secure Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full rounded-xl border border-gray-300 focus:border-primary-600 focus:ring-1 focus:ring-primary-600 outline-none px-4 py-3.5 text-sm transition-all"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{errorMsg && (
|
||||||
|
<div className="text-red-500 text-sm font-medium mt-2 px-1">
|
||||||
|
{errorMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buttons Row */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-6">
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="w-full bg-gray-50 border border-gray-200 hover:bg-gray-100 text-dark-900 font-bold text-sm py-3.5 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleRegister}
|
||||||
|
disabled={isRegistering}
|
||||||
|
className="w-full bg-primary-600 hover:bg-primary-700 text-white font-bold text-sm py-3.5 rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isRegistering ? "Creating Account..." : "Create Account"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="relative flex items-center mb-6">
|
||||||
|
<div className="flex-grow border-t border-gray-100"></div>
|
||||||
|
<span className="flex-shrink-0 mx-4 text-gray-400 text-sm">or</span>
|
||||||
|
<div className="flex-grow border-t border-gray-100"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Google Btn */}
|
||||||
|
<button
|
||||||
|
onClick={() => login()}
|
||||||
|
className="w-full flex items-center justify-center gap-3 bg-gray-50 border border-dark-200 hover:bg-gray-100 text-dark-900 font-bold text-sm py-3.5 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" viewBox="0 0 48 48">
|
||||||
|
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z" />
|
||||||
|
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z" />
|
||||||
|
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z" />
|
||||||
|
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z" />
|
||||||
|
</svg>
|
||||||
|
Login with Google
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PhoneInputScreen;
|
||||||
45
src/components/Auth/UserProfileScreen.jsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const UserProfileScreen = ({ user, onLogout }) => {
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-sm mx-auto p-4 sm:p-8 animate-fadeInUp flex flex-col items-center">
|
||||||
|
{/* Success Checkmark */}
|
||||||
|
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-6">
|
||||||
|
<svg className="w-8 h-8 text-green-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="20 6 9 17 4 12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-bold text-dark-900 mb-2">
|
||||||
|
{user.isNewRegistration ? "Successfully Registered!" : "Successfully Logged In!"}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-dark-500 mb-8 text-center">
|
||||||
|
Welcome to your road.rentals dashboard.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* User Info Card */}
|
||||||
|
<div className="w-full bg-gray-50 border border-gray-200 rounded-2xl p-6 flex flex-col items-center mb-6">
|
||||||
|
<img
|
||||||
|
src={user.picture}
|
||||||
|
alt={user.name}
|
||||||
|
className="w-20 h-20 rounded-full border-4 border-white shadow-md mb-4 object-cover"
|
||||||
|
/>
|
||||||
|
<h3 className="text-lg font-bold text-dark-900">{user.name}</h3>
|
||||||
|
<p className="text-sm text-dark-500">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logout Button */}
|
||||||
|
<button
|
||||||
|
onClick={onLogout}
|
||||||
|
className="w-full bg-red-50 hover:bg-red-100 text-red-600 font-bold text-sm py-3.5 rounded-xl transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserProfileScreen;
|
||||||
29
src/components/Cabs/CabsSection.jsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { cabsData } from "../../data/cabsData";
|
||||||
|
import { filterTabs } from "../../constants/filterTabs";
|
||||||
|
import SectionHeader from "./SectionHeader";
|
||||||
|
import VehicleGrid from "./VehicleGrid";
|
||||||
|
import FilterTabs from "./FilterTabs";
|
||||||
|
|
||||||
|
const CabsSection = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState("driver-rentals");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="fleet" className="py-16 md:py-20 px-4 sm:px-6 lg:px-8 bg-section-bg">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<SectionHeader
|
||||||
|
title="Our Fleet – Affordable to Luxury"
|
||||||
|
subtitle="Choose from our wide range of well-maintained vehicles for local trips, outstation travel, and airport transfers across Bangalore."
|
||||||
|
/>
|
||||||
|
<FilterTabs
|
||||||
|
tabs={filterTabs}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
/>
|
||||||
|
<VehicleGrid vehicles={cabsData} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CabsSection;
|
||||||
38
src/components/Cabs/FilterTabs.jsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
const FilterTabs = ({ tabs, activeTab, onTabChange }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center mb-10">
|
||||||
|
<div className="inline-flex gap-1 p-2 bg-white/40 backdrop-blur-xl rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.08)] border border-white/60 relative z-10 transition-all duration-300 hover:bg-white/50 hover:shadow-[0_8px_30px_rgb(0,0,0,0.12)]">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isActive = tab.key === activeTab;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.key}
|
||||||
|
onClick={() => !tab.disabled && onTabChange(tab.key)}
|
||||||
|
className={`
|
||||||
|
group relative px-5 py-2.5 text-sm font-medium rounded-lg
|
||||||
|
transition-all duration-250 ease-out
|
||||||
|
${
|
||||||
|
tab.disabled
|
||||||
|
? "opacity-60 cursor-default bg-transparent text-gray-400"
|
||||||
|
: isActive
|
||||||
|
? "bg-primary-600 text-white shadow-md cursor-pointer"
|
||||||
|
: "text-dark-700 hover:bg-white/40 hover:text-primary-600 cursor-pointer"
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
{tab.disabled && (
|
||||||
|
<span className="absolute -top-10 left-1/2 -translate-x-1/2 px-3 py-1 bg-dark-900 text-white text-xs font-bold rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap pointer-events-none z-10">
|
||||||
|
Coming Soon
|
||||||
|
<span className="absolute top-full left-1/2 -translate-x-1/2 border-[5px] border-transparent border-t-dark-900"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterTabs;
|
||||||
17
src/components/Cabs/SectionHeader.jsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
const SectionHeader = ({ title, subtitle }) => {
|
||||||
|
return (
|
||||||
|
<div className="text-center mb-10">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-dark-900 tracking-tight">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="mt-3 text-base text-badge-text max-w-2xl mx-auto leading-relaxed">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-4 mx-auto w-16 h-1 rounded-full bg-primary-600" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionHeader;
|
||||||
35
src/components/Cabs/VehicleCard/VehicleBadges.jsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
const featureIcons = {
|
||||||
|
"Air Conditioner": (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M12 2v10" /><path d="m4.93 10.93 1.41 1.41" /><path d="M2 18h2" /><path d="M20 18h2" /><path d="m19.07 10.93-1.41 1.41" /><path d="M22 22H2" /><path d="m8 6 4-4 4 4" /><path d="M16 18a4 4 0 0 0-8 0" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
"Audio System": (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M9 18V5l12-2v13" /><circle cx="6" cy="18" r="3" /><circle cx="18" cy="16" r="3" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
"Spacious Leg Space": (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M19 9V6a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v3" /><path d="M3 16h18" /><path d="M3 11h2a2 2 0 0 1 2 2v3" /><path d="M17 11h2a2 2 0 0 1 2 2v3" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const VehicleBadges = ({ features }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2 px-5 pb-3">
|
||||||
|
{features.map((feature) => (
|
||||||
|
<span
|
||||||
|
key={feature}
|
||||||
|
className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-badge-bg text-badge-text text-xs font-medium rounded-md"
|
||||||
|
>
|
||||||
|
{featureIcons[feature] || null}
|
||||||
|
{feature}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VehicleBadges;
|
||||||
17
src/components/Cabs/VehicleCard/VehicleCard.jsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import VehicleImage from "./VehicleImage";
|
||||||
|
import VehicleInfo from "./VehicleInfo";
|
||||||
|
import VehicleBadges from "./VehicleBadges";
|
||||||
|
import ViewDetailsBtn from "./ViewDetailsBtn";
|
||||||
|
|
||||||
|
const VehicleCard = ({ vehicle }) => {
|
||||||
|
return (
|
||||||
|
<article className="group bg-white rounded-xl shadow-card hover:shadow-card-hover transition-all duration-300 ease-out overflow-hidden">
|
||||||
|
<VehicleImage src={vehicle.image} alt={vehicle.name} />
|
||||||
|
<VehicleInfo name={vehicle.name} seats={vehicle.seats} />
|
||||||
|
<VehicleBadges features={vehicle.features} />
|
||||||
|
<ViewDetailsBtn price={vehicle.price} detailUrl={vehicle.detailUrl} />
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VehicleCard;
|
||||||
14
src/components/Cabs/VehicleCard/VehicleImage.jsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
const VehicleImage = ({ src, alt }) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-52 overflow-hidden rounded-t-xl bg-gray-100">
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className="w-full h-full object-cover transition-transform duration-500 ease-out group-hover:scale-110"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VehicleImage;
|
||||||
29
src/components/Cabs/VehicleCard/VehicleInfo.jsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
const VehicleInfo = ({ name, seats }) => {
|
||||||
|
return (
|
||||||
|
<div className="px-5 pt-4 pb-2">
|
||||||
|
<h3 className="text-lg font-semibold text-dark-900 leading-tight">
|
||||||
|
{name}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-1.5 mt-1.5 text-badge-text text-sm">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-4 h-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
|
</svg>
|
||||||
|
<span>{seats} seats</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VehicleInfo;
|
||||||
31
src/components/Cabs/VehicleCard/ViewDetailsBtn.jsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
const ViewDetailsBtn = ({ price, detailUrl }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between px-5 py-3.5 border-t border-gray-100">
|
||||||
|
<p className="text-sm text-dark-700">
|
||||||
|
Starting from{" "}
|
||||||
|
<span className="font-semibold text-dark-900">{price}</span>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="#hero"
|
||||||
|
className="inline-flex items-center gap-1 text-sm font-semibold text-primary-600 hover:text-primary-700 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
View Details
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-4 h-4 transition-transform duration-200 group-hover:translate-x-0.5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
<path d="m12 5 7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewDetailsBtn;
|
||||||
61
src/components/Cabs/VehicleGrid.jsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import VehicleCard from "./VehicleCard/VehicleCard";
|
||||||
|
|
||||||
|
const VehicleGrid = ({ vehicles }) => {
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
const visibleVehicles = showAll ? vehicles : vehicles.slice(0, 6);
|
||||||
|
if (vehicles.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-16">
|
||||||
|
<div className="text-6xl mb-4">🚗</div>
|
||||||
|
<p className="text-lg text-badge-text font-medium">
|
||||||
|
No vehicles found in this category.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-400 mt-1">
|
||||||
|
Try selecting a different filter above.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{visibleVehicles.map((vehicle, index) => (
|
||||||
|
<div
|
||||||
|
key={vehicle.id}
|
||||||
|
className="opacity-0 animate-fadeInUp"
|
||||||
|
style={{ animationDelay: `${index * 60}ms`, animationFillMode: "forwards" }}
|
||||||
|
>
|
||||||
|
<VehicleCard vehicle={vehicle} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{vehicles.length > 6 && (
|
||||||
|
<div className="flex justify-center mt-12">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAll(!showAll)}
|
||||||
|
className="inline-flex items-center gap-2 px-8 py-3.5 border-2 border-primary-600 text-primary-600 font-bold text-sm rounded-xl hover:bg-primary-600 hover:text-white transition-all duration-300 shadow-sm"
|
||||||
|
>
|
||||||
|
{showAll ? "Show Less" : "Show More"}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={`w-4 h-4 transition-transform duration-300 ${showAll ? "rotate-180" : ""}`}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m6 9 6 6 6-6"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VehicleGrid;
|
||||||
90
src/components/Footer/Footer.jsx
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import Logo from "../Logo/Logo";
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
const scrollToHero = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById("hero")?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer id="contact" className="bg-[#01172f] text-white relative border-t border-dark-900 overflow-hidden">
|
||||||
|
{/* Cityscape Silhouette Background */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-0 w-full h-[300px] pointer-events-none bg-[length:100%_100%] bg-bottom bg-no-repeat opacity-90"
|
||||||
|
style={{ backgroundImage: "url('https://ik.imagekit.io/fofu0hrhb/footerbgimage.png')" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-16 pb-8">
|
||||||
|
{/* Middle Links Grid */}
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-5 gap-8 mb-16 relative z-10">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-lg mb-6 text-white">Our Fleet</h4>
|
||||||
|
<ul className="space-y-4">
|
||||||
|
{["Sedan Cabs", "SUV Cabs", "Premium SUVs", "Luxury Rides"].map(item => (
|
||||||
|
<li key={item}><a href="#" onClick={scrollToHero} className="text-[#93c5fd] hover:text-white text-sm transition-colors">{item}</a></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-lg mb-6 text-white">Services</h4>
|
||||||
|
<ul className="space-y-4">
|
||||||
|
{["Corporate Travel", "Airport Transfers", "Family Trips", "City Tours", "Outstation Travel", "Wedding Cabs", "Holiday Packages"].map(item => (
|
||||||
|
<li key={item}><a href="#" onClick={scrollToHero} className="text-[#93c5fd] hover:text-white text-sm transition-colors">{item}</a></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-lg mb-6 text-white">Luxury Cabs</h4>
|
||||||
|
<ul className="space-y-4">
|
||||||
|
{["Luxury Sedan", "Luxury SUV", "Premium Executive Cabs"].map(item => (
|
||||||
|
<li key={item}><a href="#" onClick={scrollToHero} className="text-[#93c5fd] hover:text-white text-sm transition-colors">{item}</a></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-lg mb-6 text-white">Other Links</h4>
|
||||||
|
<ul className="space-y-4">
|
||||||
|
{["About Us", "Contact Us", "Blogs", "Cancellation Policy", "Terms & Conditions", "Privacy Policy", "Pricing Policy", "Partner With Us"].map(item => (
|
||||||
|
<li key={item}><a href="#" onClick={scrollToHero} className="text-[#93c5fd] hover:text-white text-sm transition-colors">{item}</a></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-2 lg:col-span-1">
|
||||||
|
<Logo lightText={true} className="mb-6" />
|
||||||
|
<p className="text-[#93c5fd] text-sm leading-relaxed mb-6">
|
||||||
|
We are a popular and leading Cab and Premium Tours company based out of Bangalore. road.rentals has been trusted by hundreds of happy travellers for several decades now as a renowned and acclaimed as the best travel service provider based out of Bangalore.
|
||||||
|
</p>
|
||||||
|
{/* Social Icons Placeholder */}
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<a href="#" className="w-9 h-9 rounded-full bg-[#1e293b] flex items-center justify-center text-[#93c5fd] hover:bg-primary-500 hover:text-white transition-colors">
|
||||||
|
<svg className="w-4 h-4 fill-current" viewBox="0 0 24 24"><path d="M21.582 6.186a2.684 2.684 0 0 0-1.884-1.889C18.035 3.8 12 3.8 12 3.8s-6.035 0-7.698.497a2.684 2.684 0 0 0-1.884 1.889C1.922 7.85 1.922 12 1.922 12s0 4.15.5 5.814a2.685 2.685 0 0 0 1.884 1.889c1.664.497 7.698.497 7.698.497s6.035 0 7.698-.497a2.685 2.685 0 0 0 1.884-1.889c.496-1.664.496-5.814.496-5.814s0-4.15-.496-5.814zm-14.28 9.38v-7.13L13.71 12l-6.408 3.566z" /></svg>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="w-9 h-9 rounded-full bg-[#1e293b] flex items-center justify-center text-[#93c5fd] hover:bg-primary-500 hover:text-white transition-colors">
|
||||||
|
<svg className="w-4 h-4 fill-current" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /></svg>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="w-9 h-9 rounded-full bg-[#1e293b] flex items-center justify-center text-[#93c5fd] hover:bg-primary-500 hover:text-white transition-colors">
|
||||||
|
<svg className="w-4 h-4 fill-current" viewBox="0 0 24 24"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.879V14.89h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.989C18.343 21.129 22 16.99 22 12c0-5.523-4.477-10-10-10z" /></svg>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="w-9 h-9 rounded-full bg-[#1e293b] flex items-center justify-center text-[#93c5fd] hover:bg-primary-500 hover:text-white transition-colors">
|
||||||
|
<svg className="w-4 h-4 fill-current" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 1 0 0 12.324 6.162 6.162 0 0 0 0-12.324zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm7.846-10.405a1.44 1.44 0 1 1-2.88 0 1.44 1.44 0 0 1 2.88 0z" /></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Banner */}
|
||||||
|
<div className="border-t border-dark-800 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
Copyright © 2018-2025 road.rentals Tours & Travels. All Rights Reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
212
src/components/Hero/HeroSection.jsx
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import heroBg from "../../assets/hero_bg.png";
|
||||||
|
|
||||||
|
const tripTypes = [
|
||||||
|
{
|
||||||
|
key: "round",
|
||||||
|
label: "Round Trip",
|
||||||
|
icon: (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 2l4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="M7 22l-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "oneway",
|
||||||
|
label: "One Way",
|
||||||
|
icon: (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "local",
|
||||||
|
label: "Local Trip",
|
||||||
|
icon: (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "airport",
|
||||||
|
label: "Airport Trip",
|
||||||
|
icon: (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z"/></svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const localPackages = [
|
||||||
|
"4Hr 40Km",
|
||||||
|
"8Hr 80Km",
|
||||||
|
"12Hr 120Km",
|
||||||
|
];
|
||||||
|
|
||||||
|
const HeroSection = () => {
|
||||||
|
const [activeTrip, setActiveTrip] = useState("round");
|
||||||
|
const [selectedPackage, setSelectedPackage] = useState(localPackages[0]);
|
||||||
|
|
||||||
|
const getFormFields = () => {
|
||||||
|
switch (activeTrip) {
|
||||||
|
case "round":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField icon="location" label="From City" placeholder="Enter Pickup City" />
|
||||||
|
<Divider />
|
||||||
|
<FormField icon="destination" label="To City" placeholder="Enter Drop Location" />
|
||||||
|
<Divider />
|
||||||
|
<FormField icon="calendar" label="Pickup date & time" placeholder="Select Date" />
|
||||||
|
<Divider />
|
||||||
|
<FormField icon="calendar" label="Return date & time" placeholder="Select Date" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case "oneway":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField icon="location" label="From City" placeholder="Enter Pickup City" />
|
||||||
|
<Divider />
|
||||||
|
<FormField icon="destination" label="To City" placeholder="Enter Drop Location" />
|
||||||
|
<Divider />
|
||||||
|
<FormField icon="calendar" label="Pickup date & time" placeholder="Select Date" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case "local":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField icon="location" label="From City" placeholder="Enter Pickup City" />
|
||||||
|
<Divider />
|
||||||
|
<div className="flex-1 min-w-[160px] px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-primary-600 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="20" height="14" x="2" y="7" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] text-badge-text font-medium leading-none mb-1">Select Package</p>
|
||||||
|
<select
|
||||||
|
value={selectedPackage}
|
||||||
|
onChange={(e) => setSelectedPackage(e.target.value)}
|
||||||
|
className="text-sm font-semibold text-dark-900 bg-transparent border-none outline-none cursor-pointer appearance-none"
|
||||||
|
>
|
||||||
|
{localPackages.map((pkg) => (
|
||||||
|
<option key={pkg} value={pkg}>{pkg}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<FormField icon="calendar" label="Pickup date & time" placeholder="Select Date" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case "airport":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField icon="location" label="From" placeholder="Enter Pickup Location" />
|
||||||
|
<Divider />
|
||||||
|
<FormField icon="destination" label="Airport" placeholder="Enter Airport Name" />
|
||||||
|
<Divider />
|
||||||
|
<FormField icon="calendar" label="Pickup date & time" placeholder="Select Date" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="hero" className="relative min-h-[520px] md:min-h-[580px] flex flex-col items-center justify-center overflow-hidden">
|
||||||
|
{/* Background */}
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img
|
||||||
|
src={heroBg}
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-dark-900/70 via-dark-900/50 to-dark-900/70" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16">
|
||||||
|
{/* Heading */}
|
||||||
|
<h1 className="text-center text-3xl sm:text-4xl md:text-5xl font-bold text-white leading-tight mb-10 md:mb-14 drop-shadow-lg">
|
||||||
|
Your Memorable Travel<br className="hidden sm:block" /> Experience begins here
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Booking widget */}
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
{/* Trip tabs */}
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<div className="inline-flex gap-1 p-1.5 bg-white/90 backdrop-blur-sm rounded-2xl shadow-lg border border-white/20">
|
||||||
|
{tripTypes.map((trip) => {
|
||||||
|
const isActive = trip.key === activeTrip;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={trip.key}
|
||||||
|
onClick={() => setActiveTrip(trip.key)}
|
||||||
|
className={`
|
||||||
|
flex items-center gap-2 px-4 sm:px-5 py-2.5 text-sm font-semibold rounded-xl
|
||||||
|
transition-all duration-250 ease-out cursor-pointer
|
||||||
|
${
|
||||||
|
isActive
|
||||||
|
? "bg-primary-600 text-white shadow-md shadow-primary-600/30"
|
||||||
|
: "text-dark-700 hover:bg-gray-50 hover:text-primary-600"
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{trip.icon}
|
||||||
|
<span className="hidden sm:inline">{trip.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<div className="bg-white/95 backdrop-blur-sm rounded-2xl shadow-xl p-2">
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-0">
|
||||||
|
{getFormFields()}
|
||||||
|
|
||||||
|
{/* CTA Button */}
|
||||||
|
<button className="flex-shrink-0 inline-flex items-center justify-center gap-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold text-sm px-6 py-3.5 rounded-xl transition-all duration-200 shadow-md shadow-primary-600/20 hover:shadow-lg hover:shadow-primary-600/30 cursor-pointer mx-2 sm:mx-0 sm:ml-auto">
|
||||||
|
Start Booking Now
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --- Sub-components --- */
|
||||||
|
|
||||||
|
const FormField = ({ icon, label, placeholder }) => {
|
||||||
|
const icons = {
|
||||||
|
location: (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-red-500 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||||
|
),
|
||||||
|
destination: (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-primary-600 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||||
|
),
|
||||||
|
calendar: (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-dark-700 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/></svg>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 min-w-[160px] px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{icons[icon]}
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] text-badge-text font-medium leading-none mb-1">{label}</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="text-sm font-semibold text-dark-900 placeholder:text-gray-400 bg-transparent border-none outline-none w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Divider = () => (
|
||||||
|
<div className="hidden sm:block w-px h-10 bg-gray-200 self-center flex-shrink-0" />
|
||||||
|
);
|
||||||
|
|
||||||
|
export default HeroSection;
|
||||||
35
src/components/Highlights/WhyChooseUs.jsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { highlightsData } from "../../data/homeData";
|
||||||
|
|
||||||
|
const WhyChooseUs = () => {
|
||||||
|
return (
|
||||||
|
<section id="about" className="py-16 bg-white px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-center text-dark-900 mb-10">
|
||||||
|
Why Choose road.rentals Cabs & Travels?
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
{highlightsData.map((highlight) => (
|
||||||
|
<div key={highlight.id} className="flex flex-col text-center group cursor-pointer">
|
||||||
|
<div className="rounded-xl overflow-hidden h-40 mb-4 shadow-sm group-hover:shadow-md transition-all">
|
||||||
|
<img
|
||||||
|
src={highlight.image}
|
||||||
|
alt={highlight.title}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-lg text-dark-900 mb-2 group-hover:text-primary-600 transition-colors">
|
||||||
|
{highlight.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-dark-700 text-sm leading-relaxed">
|
||||||
|
{highlight.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WhyChooseUs;
|
||||||
22
src/components/Logo/Logo.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Logo = ({ className = "", lightText = false }) => {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center gap-2 ${className}`}>
|
||||||
|
<div className="bg-primary-600 text-white rounded-lg p-1.5 flex items-center justify-center shadow-sm">
|
||||||
|
{/* Car Icon SVG */}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 sm:w-6 sm:h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2" />
|
||||||
|
<circle cx="7" cy="17" r="2" />
|
||||||
|
<path d="M9 17h6" />
|
||||||
|
<circle cx="17" cy="17" r="2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className={`text-xl sm:text-2xl font-extrabold tracking-tight ${lightText ? 'text-white' : 'text-dark-900'}`}>
|
||||||
|
road.<span className="text-primary-600">rentals</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Logo;
|
||||||
128
src/components/Navbar/Navbar.jsx
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import Logo from "../Logo/Logo";
|
||||||
|
|
||||||
|
const Navbar = ({ onLoginClick, user }) => {
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|
||||||
|
const navLinks = [
|
||||||
|
{ label: "Home", href: "#hero" },
|
||||||
|
{ label: "Our Fleet", href: "#fleet" },
|
||||||
|
{ label: "About", href: "#about" },
|
||||||
|
{ label: "Contact", href: "#contact" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Top info bar */}
|
||||||
|
<div className="bg-dark-900 text-white text-xs">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between h-9">
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<a
|
||||||
|
href="mailto:info@road.rentals"
|
||||||
|
className="flex items-center gap-1.5 hover:text-primary-200 transition-colors"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="20" height="16" x="2" y="4" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /></svg>
|
||||||
|
info@road.rentals
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="tel:+919980277773"
|
||||||
|
className="flex items-center gap-1.5 hover:text-primary-200 transition-colors"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" /></svg>
|
||||||
|
+91 7975942520
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button onClick={onLoginClick} className="hidden sm:flex items-center gap-1.5 hover:text-primary-200 transition-colors font-semibold">
|
||||||
|
{user ? (
|
||||||
|
<>
|
||||||
|
<img src={user.picture} alt={user.name} className="w-5 h-5 rounded-full object-cover" />
|
||||||
|
Hi, {user.name.split(' ')[0]}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
|
||||||
|
Login / Register
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main navbar */}
|
||||||
|
<nav className="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-gray-100 shadow-sm">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-center justify-between h-16 md:h-[72px]">
|
||||||
|
{/* Logo */}
|
||||||
|
<a href="#" className="flex-shrink-0 hover:opacity-90 transition-opacity">
|
||||||
|
<Logo />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Desktop links */}
|
||||||
|
<div className="hidden md:flex items-center gap-8">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.label}
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm font-semibold text-dark-900 hover:text-primary-600 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Book Now CTA */}
|
||||||
|
<div className="hidden md:flex items-center">
|
||||||
|
<a
|
||||||
|
href="#hero"
|
||||||
|
className="inline-flex items-center gap-2 px-5 py-2.5 border-2 border-primary-600 text-primary-600 font-semibold text-sm rounded-xl hover:bg-primary-600 hover:text-white transition-all duration-200"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="18" height="18" x="3" y="4" rx="2" ry="2" /><line x1="16" x2="16" y1="2" y2="6" /><line x1="8" x2="8" y1="2" y2="6" /><line x1="3" x2="21" y1="10" y2="10" /></svg>
|
||||||
|
Book Now
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile hamburger */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileOpen(!mobileOpen)}
|
||||||
|
className="md:hidden p-2 rounded-lg text-dark-900 hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
{mobileOpen ? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
||||||
|
) : (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="4" x2="20" y1="12" y2="12" /><line x1="4" x2="20" y1="6" y2="6" /><line x1="4" x2="20" y1="18" y2="18" /></svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile menu */}
|
||||||
|
<div
|
||||||
|
className={`md:hidden overflow-hidden transition-all duration-300 ease-out ${mobileOpen ? "max-h-80 border-t border-gray-100" : "max-h-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="px-4 py-4 space-y-1 bg-white">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.label}
|
||||||
|
href={link.href}
|
||||||
|
className="block px-4 py-2.5 rounded-lg text-sm font-semibold text-dark-900 hover:bg-primary-50 hover:text-primary-600 transition-colors"
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
<a
|
||||||
|
href="#hero"
|
||||||
|
className="block mx-4 mt-3 px-4 py-2.5 text-center bg-primary-600 text-white font-semibold text-sm rounded-xl"
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
>
|
||||||
|
Book Now
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
36
src/components/News/NewsSection.jsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { newsData } from "../../data/homeData";
|
||||||
|
|
||||||
|
const NewsSection = () => {
|
||||||
|
return (
|
||||||
|
<section className="py-16 bg-gray-50 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-center text-dark-900 mb-10">Latest News</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{newsData.map((news) => (
|
||||||
|
<div key={news.id} className="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow cursor-pointer group">
|
||||||
|
<div className="h-56 overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={news.image}
|
||||||
|
alt={news.title}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-5">
|
||||||
|
<h3 className="font-bold text-dark-900 text-base mb-4 group-hover:text-primary-600 transition-colors line-clamp-2">
|
||||||
|
{news.title}
|
||||||
|
</h3>
|
||||||
|
<div className="text-primary-600 text-sm font-semibold flex items-center justify-center gap-1">
|
||||||
|
Read More
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsSection;
|
||||||
65
src/components/Promotions/PromoBanners.jsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
const PromoBanners = () => {
|
||||||
|
return (
|
||||||
|
<section className="py-12 bg-gray-50 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{/* Banner 1: 10% OFF */}
|
||||||
|
<div className="relative rounded-2xl overflow-hidden h-48 md:h-56 group shadow-md cursor-pointer">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?auto=format&fit=crop&q=80&w=800"
|
||||||
|
alt="10% OFF"
|
||||||
|
className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-green-900/80 to-transparent p-6 sm:p-8 flex flex-col justify-center">
|
||||||
|
<h3 className="text-white text-2xl font-bold mb-2">Get 10% OFF</h3>
|
||||||
|
<p className="text-green-50 text-sm mb-4">on your first booking, use code</p>
|
||||||
|
<div className="inline-flex w-fit px-4 py-2 border border-white/50 rounded-lg text-white font-semibold text-sm backdrop-blur-sm">
|
||||||
|
FIRSTRIP
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Banner 2: Beach Getaway */}
|
||||||
|
<div className="relative rounded-2xl overflow-hidden h-48 md:h-56 group shadow-md cursor-pointer">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&q=80&w=800"
|
||||||
|
alt="Beach Getaway"
|
||||||
|
className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-primary-900/80 to-transparent p-6 sm:p-8 flex flex-col justify-center">
|
||||||
|
<h3 className="text-white text-xl md:text-2xl font-bold mb-2">Your Dream Beach Getaway Awaits!</h3>
|
||||||
|
<p className="text-blue-50 text-sm mb-4">7 days of sun, sand & pure relaxation!</p>
|
||||||
|
<button
|
||||||
|
onClick={() => document.getElementById("hero")?.scrollIntoView({ behavior: "smooth" })}
|
||||||
|
className="w-fit px-5 py-2 bg-white text-primary-900 font-bold text-sm rounded-lg hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Banner 3: Mysore */}
|
||||||
|
<div className="relative rounded-2xl overflow-hidden h-48 md:h-56 group shadow-md cursor-pointer">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1524492412937-b28074a5d7da?auto=format&fit=crop&q=80&w=800"
|
||||||
|
alt="Mysore Getaway"
|
||||||
|
className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-dark-900/80 to-transparent p-6 sm:p-8 flex flex-col justify-center">
|
||||||
|
<h3 className="text-white text-xl md:text-2xl font-bold mb-2">Royal Mysore Getaway!</h3>
|
||||||
|
<p className="text-gray-200 text-sm mb-4 line-clamp-2">Explore palaces, gardens & rich heritage in the City of Royals!</p>
|
||||||
|
<button
|
||||||
|
onClick={() => document.getElementById("hero")?.scrollIntoView({ behavior: "smooth" })}
|
||||||
|
className="w-fit px-5 py-2 bg-white text-dark-900 font-bold text-sm rounded-lg hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PromoBanners;
|
||||||
46
src/components/Reviews/ReviewsSection.jsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { reviewsData } from "../../data/homeData";
|
||||||
|
|
||||||
|
const ReviewsSection = () => {
|
||||||
|
return (
|
||||||
|
<section id="reviews" className="py-16 bg-white px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-center text-dark-900 mb-10">Recent Reviews</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
{reviewsData.map((review) => (
|
||||||
|
<div key={review.id} className="bg-white border border-gray-100 rounded-2xl p-6 shadow-sm hover:shadow-md transition-shadow">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex justify-between items-start mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<img src={review.avatar} alt={review.name} className="w-10 h-10 rounded-full" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-dark-900 text-sm">{review.name}</h4>
|
||||||
|
<span className="text-xs text-dark-400">{review.date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 bg-primary-500 text-white px-2 py-1 rounded-md text-xs font-bold">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-3 h-3 fill-current" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||||||
|
{review.rating}/5
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<h5 className="font-bold text-sm text-dark-900 mb-2">{review.title}</h5>
|
||||||
|
<p className="text-sm text-dark-700 leading-relaxed mb-3 line-clamp-4">
|
||||||
|
{review.text}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<button className="text-primary-600 text-sm font-semibold flex items-center gap-1 hover:text-primary-700">
|
||||||
|
Read More
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReviewsSection;
|
||||||
29
src/components/Services/ServicesSection.jsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { servicesData } from "../../data/homeData";
|
||||||
|
|
||||||
|
const ServicesSection = () => {
|
||||||
|
return (
|
||||||
|
<section id="services" className="py-16 bg-white px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-center text-dark-900 mb-10">Vehicle Services</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
{servicesData.map((service) => (
|
||||||
|
<div key={service.id} className="group cursor-pointer">
|
||||||
|
<div className="overflow-hidden rounded-xl h-64 sm:h-72 mb-4 shadow-sm group-hover:shadow-md transition-shadow">
|
||||||
|
<img
|
||||||
|
src={service.image}
|
||||||
|
alt={service.title}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-dark-900 text-center group-hover:text-primary-600 transition-colors">
|
||||||
|
{service.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServicesSection;
|
||||||
6
src/constants/filterTabs.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const filterTabs = [
|
||||||
|
{ key: "driver-rentals", label: "driver-rentals", disabled: false },
|
||||||
|
{ key: "self-driving", label: "self driving rental", disabled: true },
|
||||||
|
{ key: "driver-hiring", label: "driver-hiring", disabled: true },
|
||||||
|
{ key: "bike-rentals", label: "bike rentals", disabled: true },
|
||||||
|
];
|
||||||
157
src/data/cabsData.js
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
import etios from "../assets/vehicles/etios.png";
|
||||||
|
import innova from "../assets/vehicles/innova.png";
|
||||||
|
import dzire from "../assets/vehicles/dzire.png";
|
||||||
|
import innovaCrysta from "../assets/vehicles/innova_crysta.png";
|
||||||
|
import audiQ7 from "../assets/vehicles/audi_q7.png";
|
||||||
|
import camry from "../assets/vehicles/camry.png";
|
||||||
|
import fortuner from "../assets/vehicles/fortuner.png";
|
||||||
|
import corollaAltis from "../assets/vehicles/corolla_altis.png";
|
||||||
|
import bmw7Series from "../assets/vehicles/bmw_7_series.png";
|
||||||
|
import mercedesSClass from "../assets/vehicles/mercedes_s_class.png";
|
||||||
|
import toyotaCommuter from "../assets/vehicles/toyota_commuter.png";
|
||||||
|
import audiA6 from "../assets/vehicles/audi_a6.png";
|
||||||
|
import bmw5Series from "../assets/vehicles/bmw_5_series.png";
|
||||||
|
import mercedesEClass from "../assets/vehicles/mercedes_e_class.png";
|
||||||
|
|
||||||
|
export const cabsData = [
|
||||||
|
{
|
||||||
|
id: "etios-ac",
|
||||||
|
name: "Etios A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 4,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹1,500",
|
||||||
|
image: etios,
|
||||||
|
detailUrl: "/cab-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "innova-6-ac",
|
||||||
|
name: "Innova 6 A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 6,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹3,500",
|
||||||
|
image: innova,
|
||||||
|
detailUrl: "/innova-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dzire-swift-ac",
|
||||||
|
name: "Dzire Swift A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 4,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹1,500",
|
||||||
|
image: dzire,
|
||||||
|
detailUrl: "/dzire-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "innova-crysta-6-ac",
|
||||||
|
name: "Innova Crysta 6 A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 6,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹2,500",
|
||||||
|
image: innovaCrysta,
|
||||||
|
detailUrl: "/innova-crysta-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "audi-q7-ac",
|
||||||
|
name: "Audi-Q7 A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 4,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹12,500",
|
||||||
|
image: audiQ7,
|
||||||
|
detailUrl: "/audi-q7-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "camry-ac",
|
||||||
|
name: "Camry A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 4,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹6,000",
|
||||||
|
image: camry,
|
||||||
|
detailUrl: "/camry-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fortuner-ac",
|
||||||
|
name: "Toyota Fortuner A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 5,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹6,500",
|
||||||
|
image: fortuner,
|
||||||
|
detailUrl: "/fortuner-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "corolla-altis-ac",
|
||||||
|
name: "Corolla-Altis A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 4,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹4,000",
|
||||||
|
image: corollaAltis,
|
||||||
|
detailUrl: "/corolla-altis-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bmw-7-series-ac",
|
||||||
|
name: "BMW 7 Series A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 4,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹15,000",
|
||||||
|
image: bmw7Series,
|
||||||
|
detailUrl: "/bmw-7-series-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mercedes-s-class-ac",
|
||||||
|
name: "Mercedes-Benz S-Class A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 4,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹18,000",
|
||||||
|
image: mercedesSClass,
|
||||||
|
detailUrl: "/mercedes-benz-s-class-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "toyota-commuter-ac",
|
||||||
|
name: "Toyota Commuter A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 10,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹8,500",
|
||||||
|
image: toyotaCommuter,
|
||||||
|
detailUrl: "/toyota-commuter-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "audi-a6",
|
||||||
|
name: "Audi A6",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 4,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹8,000",
|
||||||
|
image: audiA6,
|
||||||
|
detailUrl: "/audi-a6-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bmw-5-series-ac",
|
||||||
|
name: "BMW 5 Series A/C",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 4,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹8,000",
|
||||||
|
image: bmw5Series,
|
||||||
|
detailUrl: "/bmw-5-series-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mercedes-e-class",
|
||||||
|
name: "Mercedes Benz E Class",
|
||||||
|
category: "cabs",
|
||||||
|
seats: 4,
|
||||||
|
features: ["Air Conditioner", "Audio System", "Spacious Leg Space"],
|
||||||
|
price: "₹14,500",
|
||||||
|
image: mercedesEClass,
|
||||||
|
detailUrl: "/mercedes-benz-e-class-car-rent-bangalore",
|
||||||
|
},
|
||||||
|
];
|
||||||
106
src/data/homeData.js
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
export const servicesData = [
|
||||||
|
{
|
||||||
|
id: "corporate",
|
||||||
|
title: "Corporate Travel",
|
||||||
|
image: "https://images.unsplash.com/photo-1554469384-e58fac16e23a?auto=format&fit=crop&q=80&w=800",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "airport",
|
||||||
|
title: "Airport Transfers",
|
||||||
|
image: "https://images.unsplash.com/photo-1436491865332-7a61a109cc05?auto=format&fit=crop&q=80&w=800",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "commute",
|
||||||
|
title: "Family Trips",
|
||||||
|
image: "https://images.unsplash.com/photo-1502307100811-6bdc0981a85b?auto=format&fit=crop&q=80&w=800",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "wedding",
|
||||||
|
title: "Wedding Cabs & Tours",
|
||||||
|
image: "https://images.unsplash.com/photo-1519225421980-715cb0215aed?auto=format&fit=crop&q=80&w=800",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const reviewsData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Sagar",
|
||||||
|
date: "7 Jan 2025",
|
||||||
|
rating: 5,
|
||||||
|
title: "Travelled to Bangalore",
|
||||||
|
text: "The giraffe terrace and kangaroo camp were amazing, the orangutan show was hilarious. Safari by car was normal, dolphin and sea lions were below expectations but may be because the...",
|
||||||
|
avatar: "https://ui-avatars.com/api/?name=Sagar&background=random"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Anish Ranjan",
|
||||||
|
date: "24 May 2025",
|
||||||
|
rating: 5,
|
||||||
|
title: "Travelled to mysore",
|
||||||
|
text: "Had an amazing team trip to Chikmagalur! We were a group of 25 and the cabs provided were super premium and well-maintained. A special shout-out to our driver, Mr. Arun Kumar. While h...",
|
||||||
|
avatar: "https://ui-avatars.com/api/?name=Anish+Ranjan&background=random"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "dheeraj gali",
|
||||||
|
date: "19/02/24",
|
||||||
|
rating: 5,
|
||||||
|
title: "Travel to Coorg",
|
||||||
|
text: "Hired a premium luxury cab for coorg trip. The vehicle is simply superb - seats, ac, condition of the vehicle everything was top notch!! Driver was very polite and helpful for us. Special thanks...",
|
||||||
|
avatar: "https://ui-avatars.com/api/?name=dheeraj+gali&background=random"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "Srinu Gonapa",
|
||||||
|
date: "12/03/24",
|
||||||
|
rating: 5,
|
||||||
|
title: "Travel to ooty",
|
||||||
|
text: "Very good and comfortable journey. 1.Excellent service and punctual timing. 2.Staff was polite and professional. 3.Cab was clean and well-maintained. 4.Highly satisfied with travel Experience",
|
||||||
|
avatar: "https://ui-avatars.com/api/?name=Srinu+Gonapa&background=random"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const highlightsData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Professional Travel Solutions",
|
||||||
|
description: "Streamlined travel services tailored for Bangalore travelers.",
|
||||||
|
image: "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&q=80&w=1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Extensive Vehicle Network",
|
||||||
|
description: "Large fleet of premium cabs for all your needs.",
|
||||||
|
image: "https://images.unsplash.com/photo-1570125909232-eb263c188f7e?auto=format&fit=crop&q=80&w=1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Transparent Pricing",
|
||||||
|
description: "Clear costs and availability with no hidden fees.",
|
||||||
|
image: "https://images.unsplash.com/photo-1501183638710-841dd1904471?auto=format&fit=crop&q=80&w=1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "Easy Booking Options",
|
||||||
|
description: "Simple reservations for regional and outstation trips.",
|
||||||
|
image: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?auto=format&fit=crop&q=80&w=1000",
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const newsData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Wedding Cab Booking in Bangalore | AC & Non AC",
|
||||||
|
image: "https://images.unsplash.com/photo-1544620347-c4fd4a3d5957?auto=format&fit=crop&q=80&w=800",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Premium SUV Cabs in Bangalore – Perfect...",
|
||||||
|
image: "https://images.unsplash.com/photo-1464219789935-c2d9d9aba644?auto=format&fit=crop&q=80&w=800",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Luxury Cabs for Outstation Trips – Comfort...",
|
||||||
|
image: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&q=80&w=800",
|
||||||
|
}
|
||||||
|
];
|
||||||
38
src/index.css
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Figtree', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #F8FAFC;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fadeInUp {
|
||||||
|
animation: fadeInUp 0.4s ease-out;
|
||||||
|
}
|
||||||
44
src/lib/api.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Single source of truth for backend communication
|
||||||
|
|
||||||
|
const BASE_URL = import.meta.env.VITE_API_URL || "";
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
post: async (path, body) => {
|
||||||
|
// Make sure we form the correct full URL (handling missing slashes if any)
|
||||||
|
const url = path.startsWith("http") ? path : `${BASE_URL}${path}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
let data;
|
||||||
|
let textResult = await response.text();
|
||||||
|
try {
|
||||||
|
data = textResult ? JSON.parse(textResult) : {};
|
||||||
|
} catch (e) {
|
||||||
|
data = { detail: "Backend Server is Unreachable (Proxy Error: " + response.status + ")" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// FastAPI typically throws Error messages under `detail`
|
||||||
|
let errMsg = "An error occurred during communication";
|
||||||
|
|
||||||
|
if (typeof data.detail === "string") {
|
||||||
|
errMsg = data.detail;
|
||||||
|
} else if (Array.isArray(data.detail)) {
|
||||||
|
// FastAPI validation errors
|
||||||
|
errMsg = data.detail.map(d => d.msg).join(", ");
|
||||||
|
} else if (data.message) {
|
||||||
|
errMsg = data.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
13
src/main.jsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { GoogleOAuthProvider } from '@react-oauth/google'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.jsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<GoogleOAuthProvider clientId="725335319736-kglnmjhjppb3563vh9tpj22u5crao0a8.apps.googleusercontent.com">
|
||||||
|
<App />
|
||||||
|
</GoogleOAuthProvider>
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
41
tailwind.config.js
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#eff3fe',
|
||||||
|
100: '#d7dffc',
|
||||||
|
200: '#b0bff9',
|
||||||
|
500: '#6b8df5',
|
||||||
|
600: '#4B70F7',
|
||||||
|
700: '#3a5cd4',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
700: '#1f2937',
|
||||||
|
800: '#1a1a2e',
|
||||||
|
900: '#111827',
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
bg: '#F3F4F6',
|
||||||
|
text: '#6B7280',
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
bg: '#F8FAFC',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
figtree: ['Figtree', 'sans-serif'],
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
card: '0 1px 3px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.05)',
|
||||||
|
'card-hover': '0 4px 16px rgba(0, 0, 0, 0.12), 0 8px 24px rgba(0, 0, 0, 0.08)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
16
vite.config.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://192.168.1.27:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||