feat: Multi-role platform expansion, mobile nav redesign, and UI polish
- Add Owner, Contractor, Vendor, Subcontractor dashboards and role-based routing - Owner now has superuser access to all Admin pages (dashboard, schedule, leaderboard) - Redesign landing page mobile menu as slide-in sidebar (replaces broken Framer Motion overlay) - Add body scroll lock to app sidebar for mobile consistency - Fix Team Schedule to match Admin Dashboard design language (ambient glows, gradient header, SpotlightCard, zinc palette) - Fix login page tab overflow — use abbreviated labels and grid layout for 6 role tabs - Fix Recharts ResponsiveContainer warnings — replace height="100%" with fixed pixel heights - Fix lightbox image viewer — unified nav bar, touch swipe support, no more overlapping text - Add AI Assistant page, People/Vendor/Document management for Owner role - Expand mock data store with contractor, vendor, and subcontractor data
This commit is contained in:
+132
-59
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { User, Briefcase, Lock, ArrowRight, AlertCircle, Home } from 'lucide-react';
|
||||
import { User, Briefcase, Lock, ArrowRight, AlertCircle, Eye, EyeOff } from 'lucide-react';
|
||||
import { SpotlightCard } from '../components/SpotlightCard';
|
||||
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
|
||||
|
||||
@@ -53,9 +53,10 @@ const RainbowButton = ({ children, onClick, type = "button", className = "" }) =
|
||||
};
|
||||
|
||||
const Login = () => {
|
||||
const [isEmployee, setIsEmployee] = useState(false);
|
||||
const [loginType, setLoginType] = useState('customer'); // 'customer', 'employee', 'owner', 'contractor', 'subcontractor'
|
||||
const [identifier, setIdentifier] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const { login } = useAuth();
|
||||
@@ -70,15 +71,28 @@ const Login = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = isEmployee ? 'employee' : 'customer';
|
||||
const result = login(identifier, password, type);
|
||||
const result = login(identifier, password, loginType);
|
||||
|
||||
if (result.success) {
|
||||
// Role-based Redirect
|
||||
if (result.role === 'CUSTOMER') {
|
||||
navigate('/');
|
||||
} else {
|
||||
navigate('/emp/fa/dashboard');
|
||||
switch (result.role) {
|
||||
case 'CUSTOMER':
|
||||
navigate('/');
|
||||
break;
|
||||
case 'OWNER':
|
||||
navigate('/owner/snapshot');
|
||||
break;
|
||||
case 'CONTRACTOR':
|
||||
navigate('/contractor/dashboard');
|
||||
break;
|
||||
case 'VENDOR':
|
||||
navigate('/vendor/dashboard');
|
||||
break;
|
||||
case 'SUBCONTRACTOR':
|
||||
navigate('/subcontractor/dashboard');
|
||||
break;
|
||||
default:
|
||||
navigate('/emp/fa/dashboard'); // Default for Employee/Admin
|
||||
}
|
||||
} else {
|
||||
setError(result.message);
|
||||
@@ -87,17 +101,33 @@ const Login = () => {
|
||||
|
||||
const fillDemo = (role) => {
|
||||
if (role === 'customer') {
|
||||
setIsEmployee(false);
|
||||
setLoginType('customer');
|
||||
setIdentifier('alice');
|
||||
setPassword('password');
|
||||
} else if (role === 'agent') {
|
||||
setIsEmployee(true);
|
||||
setLoginType('employee');
|
||||
setIdentifier('FA001');
|
||||
setPassword('password');
|
||||
} else if (role === 'admin') {
|
||||
setIsEmployee(true);
|
||||
setLoginType('employee');
|
||||
setIdentifier('ADM01');
|
||||
setPassword('password');
|
||||
} else if (role === 'owner') {
|
||||
setLoginType('owner');
|
||||
setIdentifier('justin');
|
||||
setPassword('password');
|
||||
} else if (role === 'contractor') {
|
||||
setLoginType('contractor');
|
||||
setIdentifier('mike');
|
||||
setPassword('password');
|
||||
} else if (role === 'vendor') {
|
||||
setLoginType('vendor');
|
||||
setIdentifier('abc_supply');
|
||||
setPassword('password');
|
||||
} else if (role === 'subcontractor') {
|
||||
setLoginType('subcontractor');
|
||||
setIdentifier('carlos');
|
||||
setPassword('password');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,96 +140,139 @@ const Login = () => {
|
||||
<div className="absolute bottom-[-20%] right-[-20%] w-[70%] h-[70%] bg-blue-500/5 dark:bg-blue-900/10 blur-[150px] rounded-full opacity-50"></div>
|
||||
</div>
|
||||
|
||||
{/* Autofill CSS Override */}
|
||||
<style>{`
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 30px #f4f4f5 inset !important;
|
||||
-webkit-text-fill-color: #18181b !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 30px #18181b inset !important; /* zinc-900 matches dark mode inputs */
|
||||
-webkit-text-fill-color: white !important;
|
||||
}
|
||||
}
|
||||
.dark input:-webkit-autofill,
|
||||
.dark input:-webkit-autofill:hover,
|
||||
.dark input:-webkit-autofill:focus,
|
||||
.dark input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 30px #1c1917 inset !important; /* Slightly lighter than pure black for input bg */
|
||||
-webkit-text-fill-color: white !important;
|
||||
caret-color: white !important;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
|
||||
<div className="w-full max-w-md z-10 relative">
|
||||
<div className="w-full max-w-lg z-10 relative">
|
||||
<SpotlightCard className="bg-white/60 dark:bg-black/30 backdrop-blur-3xl border-0 ring-1 ring-black/5 dark:ring-white/5 shadow-2xl dark:shadow-2xl">
|
||||
<div className="p-8">
|
||||
<div className="text-center mb-10 pt-2">
|
||||
<img src={Logo} alt="LynkedUp Pro" className="w-20 h-20 mx-auto mb-6 object-contain drop-shadow-2xl" />
|
||||
<h1 className="text-3xl font-black text-zinc-900 dark:text-white mb-2 tracking-tighter">
|
||||
<div className="p-6 md:p-10"> {/* ADJUSTED PADDING FOR MOBILE */}
|
||||
<div className="text-center mb-8 md:mb-10 pt-2">
|
||||
<img src={Logo} alt="LynkedUp Pro" className="w-20 md:w-24 h-20 md:h-24 mx-auto mb-4 md:mb-6 object-contain drop-shadow-2xl" />
|
||||
<h1 className="text-2xl md:text-3xl font-black text-zinc-900 dark:text-white mb-2 tracking-tighter">
|
||||
Welcome Back
|
||||
</h1>
|
||||
<p className="text-zinc-500 font-medium">Sign in to your LynkedUp Pro account</p>
|
||||
<p className="text-zinc-500 font-medium text-base md:text-lg">Sign in to your LynkedUp Pro account</p>
|
||||
</div>
|
||||
|
||||
{/* Neo-Toggle */}
|
||||
<div className="flex p-1.5 bg-zinc-100 dark:bg-black/40 rounded-xl mb-8 border border-zinc-200 dark:border-white/5 mx-1">
|
||||
<button
|
||||
onClick={() => setIsEmployee(false)}
|
||||
className={`flex-1 flex items-center justify-center py-2.5 text-xs font-bold uppercase tracking-wider rounded-lg transition-all duration-300 ${!isEmployee
|
||||
? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm border border-black/5 dark:border-white/5'
|
||||
: 'text-zinc-400 dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400'}`}
|
||||
>
|
||||
<User size={14} className="mr-2" />
|
||||
Customer
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsEmployee(true)}
|
||||
className={`flex-1 flex items-center justify-center py-2.5 text-xs font-bold uppercase tracking-wider rounded-lg transition-all duration-300 ${isEmployee
|
||||
? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm border border-black/5 dark:border-white/5'
|
||||
: 'text-zinc-400 dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400'}`}
|
||||
>
|
||||
<Briefcase size={14} className="mr-2" />
|
||||
Employee
|
||||
</button>
|
||||
{/* Role Tabs — 3-col grid on mobile, 6-col on desktop */}
|
||||
<div className="grid grid-cols-3 md:grid-cols-6 gap-1.5 p-1.5 bg-zinc-100 dark:bg-black/40 rounded-xl mb-8 md:mb-10 border border-zinc-200 dark:border-white/5">
|
||||
{[
|
||||
{ key: 'customer', label: 'Customer' },
|
||||
{ key: 'employee', label: 'Employee' },
|
||||
{ key: 'owner', label: 'Owner' },
|
||||
{ key: 'contractor', label: 'Contract.' },
|
||||
{ key: 'vendor', label: 'Vendor' },
|
||||
{ key: 'subcontractor', label: 'Sub-Con' },
|
||||
].map(({ key, label }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setLoginType(key)}
|
||||
className={`flex items-center justify-center py-2.5 px-1 text-[10px] font-bold uppercase rounded-lg transition-all duration-300 whitespace-nowrap ${loginType === key
|
||||
? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm border border-black/5 dark:border-white/5'
|
||||
: 'text-zinc-400 dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400'}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<form onSubmit={handleLogin} className="space-y-6 md:space-y-8">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold text-zinc-500 ml-1 uppercase tracking-widest">
|
||||
{isEmployee ? 'Employee ID' : 'Username'}
|
||||
<label className="text-xs font-bold text-zinc-500 ml-1 uppercase tracking-widest">
|
||||
{loginType === 'employee' ? 'Employee ID' : 'Username'}
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-zinc-900 dark:group-focus-within:text-white transition-colors">
|
||||
{isEmployee ? <Briefcase size={18} /> : <User size={18} />}
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-zinc-900 dark:group-focus-within:text-white transition-colors z-10">
|
||||
{loginType === 'customer' ? <User size={20} /> : <Briefcase size={20} />}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={identifier}
|
||||
onChange={(e) => setIdentifier(e.target.value)}
|
||||
className="w-full bg-zinc-100 dark:bg-black/20 border border-zinc-200 dark:border-white/5 text-zinc-900 dark:text-white rounded-xl py-4 pl-12 pr-4 focus:outline-none focus:ring-1 focus:ring-black/10 dark:focus:ring-white/20 focus:bg-white dark:focus:bg-black/40 transition-all placeholder:text-zinc-400 dark:placeholder:text-zinc-700 font-medium"
|
||||
placeholder={isEmployee ? "e.g., FA001" : "e.g., alice"}
|
||||
className="w-full bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-900 dark:text-white rounded-xl py-4 pl-12 pr-4 focus:outline-none focus:ring-1 focus:ring-black/10 dark:focus:ring-white/20 focus:bg-white dark:focus:bg-zinc-900 transition-all placeholder:text-zinc-400 dark:placeholder:text-zinc-700 font-medium relative z-0"
|
||||
placeholder={loginType === 'employee' ? "e.g., FA001" : "e.g., username"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold text-zinc-500 ml-1 uppercase tracking-widest">Password</label>
|
||||
<div className="flex justify-between items-center ml-1">
|
||||
<label className="text-xs font-bold text-zinc-500 uppercase tracking-widest">Password</label>
|
||||
<button type="button" className="text-xs font-semibold text-zinc-400 hover:text-zinc-600 dark:text-zinc-500 dark:hover:text-zinc-300 transition-colors">
|
||||
Forgot Password?
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative group">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-zinc-900 dark:group-focus-within:text-white transition-colors">
|
||||
<Lock size={18} />
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-zinc-900 dark:group-focus-within:text-white transition-colors z-10">
|
||||
<Lock size={20} />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-zinc-100 dark:bg-black/20 border border-zinc-200 dark:border-white/5 text-zinc-900 dark:text-white rounded-xl py-4 pl-12 pr-4 focus:outline-none focus:ring-1 focus:ring-black/10 dark:focus:ring-white/20 focus:bg-white dark:focus:bg-black/40 transition-all placeholder:text-zinc-400 dark:placeholder:text-zinc-700 font-medium"
|
||||
className="w-full bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-900 dark:text-white rounded-xl py-4 pl-12 pr-12 focus:outline-none focus:ring-1 focus:ring-black/10 dark:focus:ring-white/20 focus:bg-white dark:focus:bg-zinc-900 transition-all placeholder:text-zinc-400 dark:placeholder:text-zinc-700 font-medium relative z-0"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors z-10"
|
||||
>
|
||||
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center space-x-2 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 p-3 rounded-xl text-xs font-bold uppercase tracking-wide">
|
||||
<AlertCircle size={16} />
|
||||
<div className="flex items-center space-x-2 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 p-4 rounded-xl text-sm font-bold uppercase tracking-wide">
|
||||
<AlertCircle size={18} />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RainbowButton type="submit" className="mt-2 text-white">
|
||||
<RainbowButton type="submit" className="mt-4 text-white py-4 md:py-5 text-base md:text-lg">
|
||||
<span>Sign In</span>
|
||||
<ArrowRight size={18} />
|
||||
<ArrowRight size={20} />
|
||||
</RainbowButton>
|
||||
</form>
|
||||
|
||||
{/* Demo Quick Links */}
|
||||
<div className="mt-8 pt-6 border-t border-zinc-200 dark:border-white/5">
|
||||
<div className="mt-8 md:mt-10 pt-6 md:pt-8 border-t border-zinc-200 dark:border-white/5">
|
||||
<p className="text-[10px] text-center text-zinc-500 uppercase tracking-widest font-bold mb-4">Quick Demo Access</p>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<button onClick={() => fillDemo('customer')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-3 py-2 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Customer</button>
|
||||
<button onClick={() => fillDemo('agent')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-3 py-2 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Agent</button>
|
||||
<button onClick={() => fillDemo('admin')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-3 py-2 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Admin</button>
|
||||
<div className="grid grid-cols-3 md:grid-cols-4 gap-2.5">
|
||||
<button onClick={() => fillDemo('customer')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-2 py-2.5 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Customer</button>
|
||||
<button onClick={() => fillDemo('agent')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-2 py-2.5 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Agent</button>
|
||||
<button onClick={() => fillDemo('admin')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-2 py-2.5 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Admin</button>
|
||||
<button onClick={() => fillDemo('owner')} className="text-[10px] font-bold uppercase tracking-wider bg-amber-100 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-500/20 text-amber-600 dark:text-amber-400 px-2 py-2.5 rounded-lg hover:bg-amber-200 dark:hover:bg-amber-900/30 transition-colors">Owner</button>
|
||||
<button onClick={() => fillDemo('contractor')} className="text-[10px] font-bold uppercase tracking-wider bg-blue-100 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-500/20 text-blue-600 dark:text-blue-400 px-2 py-2.5 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-900/30 transition-colors">Contractor</button>
|
||||
<button onClick={() => fillDemo('vendor')} className="text-[10px] font-bold uppercase tracking-wider bg-cyan-100 dark:bg-cyan-900/20 border border-cyan-200 dark:border-cyan-500/20 text-cyan-600 dark:text-cyan-400 px-2 py-2.5 rounded-lg hover:bg-cyan-200 dark:hover:bg-cyan-900/30 transition-colors">Vendor</button>
|
||||
<button onClick={() => fillDemo('subcontractor')} className="text-[10px] font-bold uppercase tracking-wider bg-purple-100 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-500/20 text-purple-600 dark:text-purple-400 px-2 py-2.5 rounded-lg hover:bg-purple-200 dark:hover:bg-purple-900/30 transition-colors">Sub-Con</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user