diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7467243..0f7dc13 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,8 @@ import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; -import Sidebar from "@/components/Sidebar"; import { Toaster } from "react-hot-toast"; +import LayoutContent from "@/components/LayoutContent"; const inter = Inter({ variable: "--font-inter", @@ -36,10 +36,7 @@ export default function RootLayout({ return ( - -
- {children} -
+ {children} diff --git a/src/app/login/layout.tsx b/src/app/login/layout.tsx new file mode 100644 index 0000000..724a46d --- /dev/null +++ b/src/app/login/layout.tsx @@ -0,0 +1,29 @@ +import type { Metadata, Viewport } from "next"; +import { Inter } from "next/font/google"; +import "../globals.css"; + +const inter = Inter({ + variable: "--font-inter", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "JAIBEN Mobility - Login", + description: "Login to JAIBEN Mobility EV Rental Platform", +}; + +export const viewport: Viewport = { + themeColor: "#ffffff", + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, +}; + +export default function LoginLayout({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..8bfda5a --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,227 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { users } from '@/data/mockData'; +import { Zap, ArrowRight, Bike, Wallet, Shield, Users, Calculator, Store, Truck } from 'lucide-react'; + +const demoUsers = [ + { email: 'superadmin@jaiben.com', role: 'admin', label: 'Super Admin', icon: Shield, color: 'bg-accent' }, + { email: 'admin@jaiben.com', role: 'manager', label: 'Admin Manager', icon: Users, color: 'bg-blue-500' }, + { email: 'staff@jaiben.com', role: 'staff', label: 'Front Desk', icon: Users, color: 'bg-purple-500' }, + { email: 'accountant@jaiben.com', role: 'accountant', label: 'Accountant', icon: Calculator, color: 'bg-green-500' }, + { email: 'investor@email.com', role: 'investor', label: 'Investor', icon: Wallet, color: 'bg-amber-500' }, +]; + +export default function LoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + await new Promise(resolve => setTimeout(resolve, 800)); + + const user = users.find(u => u.email === email); + + if (user && password === 'demo123') { + sessionStorage.setItem('authToken', 'demo-token'); + sessionStorage.setItem('userRole', user.role); + sessionStorage.setItem('userName', user.name); + + switch (user.role) { + case 'admin': + case 'manager': + case 'staff': + router.push('/admin'); + break; + case 'accountant': + router.push('/admin'); + break; + case 'investor': + router.push('/investor'); + break; + case 'biker': + router.push('/biker'); + break; + case 'swap-station': + router.push('/swapstation'); + break; + case 'merchant': + router.push('/merchant'); + break; + default: + router.push('/'); + } + } else { + setError('Invalid email or password. Try demo123'); + } + + setLoading(false); + }; + + const handleQuickLogin = async (userEmail: string) => { + setLoading(true); + await new Promise(resolve => setTimeout(resolve, 500)); + + const user = users.find(u => u.email === userEmail); + if (user) { + sessionStorage.setItem('authToken', 'demo-token'); + sessionStorage.setItem('userRole', user.role); + sessionStorage.setItem('userName', user.name); + + switch (user.role) { + case 'admin': + case 'manager': + case 'staff': + router.push('/admin'); + break; + case 'accountant': + router.push('/admin'); + break; + case 'investor': + router.push('/investor'); + break; + case 'biker': + router.push('/biker'); + break; + case 'swap-station': + router.push('/swapstation'); + break; + case 'merchant': + router.push('/merchant'); + break; + default: + router.push('/'); + } + } + setLoading(false); + }; + + return ( +
+
+
+ +
+ +
+
+

Welcome Back

+

Sign in to continue to your dashboard

+
+ +
+
+ + setEmail(e.target.value)} + required + className="w-full px-4 py-3 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-accent focus:border-accent transition-all" + placeholder="Enter your email" + /> +
+ +
+ + setPassword(e.target.value)} + required + className="w-full px-4 py-3 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-accent focus:border-accent transition-all" + placeholder="Enter your password" + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+ +
+

Quick Login as Demo User:

+
+ {demoUsers.map((user) => { + const Icon = user.icon; + return ( + + ); + })} +
+
+
+ +
+ +
+ +
+

+ © 2024 JAIBEN Mobility Ltd. All rights reserved. +

+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 986010a..c0bb8a6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,80 +1,182 @@ -import Image from 'next/image'; -import StatCard from '@/components/StatCard'; -import BikeCard from '@/components/BikeCard'; -import TransactionList from '@/components/TransactionList'; -import { bikes, rentals, transactions, stats, users } from '@/data/mockData'; -import { Bike, Wallet, Footprints, Clock, Zap, RotateCcw, CreditCard, FileText, Share2 } from 'lucide-react'; +'use client'; -export default function Home() { - const activeRental = rentals.find(r => r.status === 'active'); - const currentBike = bikes.find(b => b.id === activeRental?.bikeId); - const currentUser = users[0]; +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { users } from '@/data/mockData'; +import { Zap, ArrowRight, Bike, Wallet } from 'lucide-react'; + +export default function LandingPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + + const handleLogin = async (email: string) => { + setLoading(true); + await new Promise(resolve => setTimeout(resolve, 500)); + + const user = users.find(u => u.email === email); + if (user) { + sessionStorage.setItem('authToken', 'demo-token'); + sessionStorage.setItem('userRole', user.role); + sessionStorage.setItem('userName', user.name); + + switch (user.role) { + case 'admin': + case 'manager': + case 'staff': + router.push('/admin'); + break; + case 'accountant': + router.push('/admin'); + break; + case 'investor': + router.push('/investor'); + break; + case 'biker': + router.push('/biker'); + break; + case 'swap-station': + router.push('/swapstation'); + break; + case 'merchant': + router.push('/merchant'); + break; + default: + router.push('/login'); + } + } + setLoading(false); + }; return ( -
-
-
-
- {currentUser.name.charAt(0)} -
-
-

Welcome back, {currentUser.name.split(' ')[0]}!

-

Here's your rental status

-
-
-
- -
- - - - -
- - {currentBike && ( -
-

Current Bike

- -
- )} - -
-
-

Quick Actions

-
- - - - -
-
- -
-

Recent Transactions

- t.userId === 'u1')} compact /> -
-
- -
+
+
-

Refer a friend

-

Earn ৳500 Credit

-

Share your referral link and earn free rides

+

JAIBEN

+

Mobility Ltd

-
-
+ + +
+
+
+
+ Bangladesh's Leading EV Platform +
+

+ Electric Vehicle +
Rental + Made Simple +

+

+ Rent, Rent-to-Own, or Invest in EVs. Join Bangladesh's fastest growing + electric mobility ecosystem with FOCO model for investors. +

+
+ +
+ + + + + +
+ +
+
+
500+
+
Active Bikers
+
+
+
200+
+
EV Fleet
+
+
+
50+
+
Investors
+
+
+
18%
+
Avg. ROI
+
+
+ +
+
+
+

Ready to join the EV revolution?

+

Start renting or investing today and be part of Bangladesh's sustainable future.

+
+
+ + +
+
+
+
+
+ +
+

+ © 2024 JAIBEN Mobility Ltd. All rights reserved. +

+
); } \ No newline at end of file diff --git a/src/components/LayoutContent.tsx b/src/components/LayoutContent.tsx new file mode 100644 index 0000000..7a300fc --- /dev/null +++ b/src/components/LayoutContent.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import Sidebar from "@/components/Sidebar"; + +interface LayoutContentProps { + children: React.ReactNode; +} + +export default function LayoutContent({ children }: LayoutContentProps) { + const pathname = usePathname(); + const showSidebar = pathname !== "/" && pathname !== "/login"; + return ( + <> + {showSidebar && } +
+ {children} +
+ + ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 0d52efe..8b9c3cd 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -154,7 +154,20 @@ export default function Sidebar() {

Admin User

admin@jaiben.com

- diff --git a/src/data/mockData.ts b/src/data/mockData.ts index 83b16bd..d7dde02 100644 --- a/src/data/mockData.ts +++ b/src/data/mockData.ts @@ -3,7 +3,7 @@ export interface User { name: string; email: string; phone: string; - role: 'biker' | 'admin' | 'manager' | 'staff' | 'accountant' | 'investor' | 'shop' | 'merchant'; + role: 'biker' | 'admin' | 'manager' | 'staff' | 'accountant' | 'investor' | 'swap-station' | 'merchant'; avatar?: string; status: 'active' | 'pending' | 'inactive'; createdAt: string; @@ -164,7 +164,7 @@ export const users: User[] = [ { id: 'u5', name: 'Staff User', email: 'staff@jaiben.com', phone: '01710000003', role: 'staff', status: 'active', createdAt: '2023-07-01' }, { id: 'u6', name: 'Accountant User', email: 'accountant@jaiben.com', phone: '01710000004', role: 'accountant', status: 'active', createdAt: '2023-07-01' }, { id: 'u7', name: 'Investor User', email: 'investor@email.com', phone: '01720000001', role: 'investor', status: 'active', createdAt: '2023-08-01' }, - { id: 'u8', name: 'Shop Owner', email: 'shop@email.com', phone: '01730000001', role: 'shop', status: 'active', createdAt: '2023-09-01' }, + { id: 'u8', name: 'Swap Station Owner', email: 'swap@jaiben.com', phone: '01730000001', role: 'swap-station', status: 'active', createdAt: '2023-09-01' }, { id: 'u9', name: 'Merchant User', email: 'merchant@email.com', phone: '01740000001', role: 'merchant', status: 'active', createdAt: '2023-10-01' }, ]; diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..fa3b4c4 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,19 @@ +export const isAuthenticated = (): boolean => { + return typeof window !== 'undefined' && !!sessionStorage.getItem('authToken'); +}; + +export const getUserRole = (): string | null => { + return typeof window !== 'undefined' ? sessionStorage.getItem('userRole') : null; +}; + +export const getUserName = (): string | null => { + return typeof window !== 'undefined' ? sessionStorage.getItem('userName') : null; +}; + +export const logout = () => { + if (typeof window !== 'undefined') { + sessionStorage.removeItem('authToken'); + sessionStorage.removeItem('userRole'); + sessionStorage.removeItem('userName'); + } +}; \ No newline at end of file diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..b2ec571 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,115 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { isAuthenticated, getUserRole } from '@/lib/auth'; + +// Define protected routes and their corresponding roles +const protectedRoutes: { [key: string]: string[] } = { + '/admin': ['admin', 'manager', 'staff', 'accountant'], + '/investor': ['investor'], + '/swapstation': ['swapstation'], + '/merchant': ['merchant'], + '/biker': ['biker'], +}; + +// Define public routes that don't require authentication +const publicRoutes = ['/login', '/', '/api/', '/_next/']; + +export async function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + // Check if the route is public + const isPublicRoute = publicRoutes.some(route => + pathname.startsWith(route) + ); + + if (isPublicRoute) { + return NextResponse.next(); + } + + // Check if the user is authenticated + const isAuth = isAuthenticated(); + + // If trying to access login page while authenticated, redirect to appropriate dashboard + if (pathname === '/login' && isAuth) { + const role = getUserRole(); + if (!role) { + // If no role found, logout and redirect to login + // In a real app, you'd clear the session and redirect + // For demo, we'll just redirect to login (but this would cause a loop, so we'll go to home) + return NextResponse.redirect(new URL('/', request.url)); + } + + // Redirect based on role + let redirectUrl = '/'; // default + + switch (role) { + case 'admin': + case 'manager': + case 'staff': + redirectUrl = '/admin'; + break; + case 'accountant': + redirectUrl = '/admin/accounting'; + break; + case 'investor': + redirectUrl = '/investor'; + break; + case 'biker': + redirectUrl = '/'; + break; + case 'swapstation': + redirectUrl = '/swapstation'; + break; + case 'merchant': + redirectUrl = '/merchant'; + break; + default: + redirectUrl = '/'; + } + + return NextResponse.redirect(new URL(redirectUrl, request.url)); + } + + // If not authenticated and trying to access a protected route, redirect to login + if (!isAuth) { + // Check if the route is protected + const isProtectedRoute = Object.keys(protectedRoutes).some(prefix => + pathname.startsWith(prefix) + ); + + if (isProtectedRoute) { + return NextResponse.redirect(new URL('/login', request.url)); + } + } + + // If authenticated, check if the user has access to the specific route + if (isAuth) { + const role = getUserRole(); + if (role) { + // Check if the route is protected and if the user's role is allowed + for (const [prefix, allowedRoles] of Object.entries(protectedRoutes)) { + if (pathname.startsWith(prefix) && !allowedRoles.includes(role)) { + // User is authenticated but doesn't have permission for this route + // Redirect to home or show an error - for demo, we'll redirect to home + return NextResponse.redirect(new URL('/', request.url)); + } + } + } + } + + return NextResponse.next(); +} + +// Configure middleware to run on specific paths +export const config = { + matcher: [ + /* + * Match all request paths except: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - public folder + */ + '/((?!_next/static|_next/image|favicon.ico|public).*)', + ], +}; \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..77798a1 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,29 @@ +// tailwind.config.js +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{js,ts,jsx,tsx}", + "./app/**/*.{js,ts,jsx,tsx}" + ], + darkMode: "class", // enable class-based dark mode + theme: { + extend: { + colors: { + // Brand-specific colors used in UI components + biker: "#1e3a8a", // deep blue for Biker theme + swapstation: "#7c3aed", // purple for Shop/Merchant theme + accent: "#6366f1", // primary accent (indigo) + "accent-dark": "#4f46e5", // darker accent for hover states + // Additional subtle shades for UI elements + "bg-dark": "#1f2937", + "bg-darker": "#111827", + "text-muted": "#9ca3af" + }, + backgroundImage: { + // optional gradient for hero background + "hero-gradient": "radial-gradient(circle at top left, #1e293b, #111827)" + } + } + }, + plugins: [] +};