378 lines
24 KiB
React
378 lines
24 KiB
React
import React, { useState, useRef } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import { User, Briefcase, Lock, ArrowRight, AlertCircle, Eye, EyeOff, Zap } from 'lucide-react';
|
|
import { SpotlightCard } from '../components/SpotlightCard';
|
|
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
|
|
|
|
|
|
// Rainbow Button Component
|
|
const RainbowButton = ({ children, onClick, type = "button", className = "" }) => {
|
|
const btnRef = useRef(null);
|
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
const [opacity, setOpacity] = useState(0);
|
|
|
|
const handleMouseMove = (e) => {
|
|
if (!btnRef.current) return;
|
|
const rect = btnRef.current.getBoundingClientRect();
|
|
setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
|
};
|
|
|
|
return (
|
|
<button
|
|
ref={btnRef}
|
|
onClick={onClick}
|
|
type={type}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseEnter={() => setOpacity(1)}
|
|
onMouseLeave={() => setOpacity(0)}
|
|
onFocus={() => setOpacity(1)}
|
|
onBlur={() => setOpacity(0)}
|
|
className={`relative group w-full py-4 rounded-xl font-bold text-white overflow-hidden transition-all duration-300 transform active:scale-[0.98] ${className}`}
|
|
>
|
|
{/* Rainbow Glow Layer */}
|
|
<div
|
|
className='pointer-events-none absolute -inset-px opacity-0 transition duration-300'
|
|
style={{
|
|
opacity,
|
|
background: `conic-gradient(from 0deg, #ff0000, #ff8800, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)`,
|
|
WebkitMaskImage: `radial-gradient(150px circle at ${position.x}px ${position.y}px, black, transparent 80%)`,
|
|
maskImage: `radial-gradient(150px circle at ${position.x}px ${position.y}px, black, transparent 80%)`,
|
|
}}
|
|
/>
|
|
|
|
{/* Button Background (Glass/Neo) */}
|
|
<div className="absolute inset-[1px] rounded-[11px] bg-zinc-900 border border-white/10 group-hover:bg-zinc-800 transition-colors z-0" />
|
|
|
|
{/* Content */}
|
|
<div className="relative z-10 flex items-center justify-center space-x-2">
|
|
{children}
|
|
</div>
|
|
</button>
|
|
);
|
|
};
|
|
|
|
// Demo account roster — each entry maps directly to MOCK_USERS credentials
|
|
const DEMO_ACCOUNTS = {
|
|
customer: [
|
|
{ name: 'Alice Carter', loginType: 'customer', id: 'alice', pw: 'password' },
|
|
],
|
|
employee: [
|
|
{ name: 'Wade Hollis — Admin', loginType: 'employee', id: 'LUP-1010', pw: 'password' },
|
|
{ name: 'Darlene Brooks — Admin', loginType: 'employee', id: 'LUP-1011', pw: 'password' },
|
|
{ name: 'Roy Schaefer — Admin', loginType: 'employee', id: 'LUP-1012', pw: 'password' },
|
|
{ name: 'Cody Tatum — Agent', loginType: 'employee', id: 'LUP-1040', pw: 'password' },
|
|
{ name: 'Hannah Reyes — Agent', loginType: 'employee', id: 'LUP-1041', pw: 'password' },
|
|
{ name: 'Travis Boone — Agent', loginType: 'employee', id: 'LUP-1042', pw: 'password' },
|
|
{ name: 'Shelby Greer — Agent', loginType: 'employee', id: 'LUP-1043', pw: 'password' },
|
|
{ name: 'Dalton Pruitt — Agent', loginType: 'employee', id: 'LUP-1044', pw: 'password' },
|
|
],
|
|
owner: [
|
|
{ name: 'Justin Johnson', loginType: 'owner', id: 'justin', pw: 'password' },
|
|
{ name: 'Diana Reeves', loginType: 'owner', id: 'diana', pw: 'password' },
|
|
],
|
|
contractor: [
|
|
{ name: 'Mike Brennan', loginType: 'contractor', id: 'mike', pw: 'password' },
|
|
],
|
|
subcontractor: [
|
|
{ name: 'Carlos Mendoza', loginType: 'subcontractor', id: 'carlos', pw: 'password' },
|
|
{ name: 'Maya Patel', loginType: 'subcontractor', id: 'maya', pw: 'password' },
|
|
{ name: 'Dwayne Boudreaux', loginType: 'subcontractor', id: 'dwayne', pw: 'password' },
|
|
{ name: 'Jennifer Castillo', loginType: 'subcontractor', id: 'jennifer', pw: 'password' },
|
|
{ name: 'Greg Alston', loginType: 'subcontractor', id: 'greg', pw: 'password' },
|
|
],
|
|
};
|
|
|
|
// Color palette per demo role chip
|
|
const DEMO_ROLE_STYLES = {
|
|
customer: { chip: 'bg-zinc-100 dark:bg-zinc-900/50 border-zinc-200 dark:border-white/5 text-zinc-500 hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300', active: 'bg-zinc-200 dark:bg-zinc-800 border-zinc-400 dark:border-white/20 text-zinc-900 dark:text-white' },
|
|
employee: { chip: 'bg-zinc-100 dark:bg-zinc-900/50 border-zinc-200 dark:border-white/5 text-zinc-500 hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300', active: 'bg-zinc-200 dark:bg-zinc-800 border-zinc-400 dark:border-white/20 text-zinc-900 dark:text-white' },
|
|
owner: { chip: 'bg-amber-100 dark:bg-amber-900/20 border-amber-200 dark:border-amber-500/20 text-amber-600 dark:text-amber-400 hover:bg-amber-200 dark:hover:bg-amber-900/30', active: 'bg-amber-200 dark:bg-amber-900/40 border-amber-400 dark:border-amber-400/50 text-amber-800 dark:text-amber-300' },
|
|
contractor: { chip: 'bg-blue-100 dark:bg-blue-900/20 border-blue-200 dark:border-blue-500/20 text-blue-600 dark:text-blue-400 hover:bg-blue-200 dark:hover:bg-blue-900/30', active: 'bg-blue-200 dark:bg-blue-900/40 border-blue-400 dark:border-blue-400/50 text-blue-800 dark:text-blue-300' },
|
|
subcontractor: { chip: 'bg-purple-100 dark:bg-purple-900/20 border-purple-200 dark:border-purple-500/20 text-purple-600 dark:text-purple-400 hover:bg-purple-200 dark:hover:bg-purple-900/30', active: 'bg-purple-200 dark:bg-purple-900/40 border-purple-400 dark:border-purple-400/50 text-purple-800 dark:text-purple-300' },
|
|
};
|
|
|
|
const DEMO_ROLE_LABELS = {
|
|
customer: 'Customer',
|
|
employee: 'Employee',
|
|
owner: 'Owner',
|
|
contractor: 'Contractor',
|
|
subcontractor: 'Sub-Con',
|
|
};
|
|
|
|
const Login = () => {
|
|
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 [expandedDemoRole, setExpandedDemoRole] = useState(null);
|
|
|
|
const { login } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
const handleLogin = (e) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
if (!identifier || !password) {
|
|
setError('Please fill in all fields');
|
|
return;
|
|
}
|
|
|
|
const result = login(identifier, password, loginType);
|
|
|
|
if (result.success) {
|
|
// Role-based Redirect
|
|
switch (result.role) {
|
|
case 'CUSTOMER':
|
|
navigate('/portal/dashboard');
|
|
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);
|
|
}
|
|
};
|
|
|
|
// Called when a person is selected from the demo picker — logs in immediately
|
|
const handleDemoLogin = (account) => {
|
|
setError('');
|
|
const result = login(account.id, account.pw, account.loginType);
|
|
if (result.success) {
|
|
switch (result.role) {
|
|
case 'CUSTOMER': navigate('/portal/dashboard'); 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');
|
|
}
|
|
} else {
|
|
setError(result.message);
|
|
}
|
|
};
|
|
|
|
const toggleDemoRole = (roleKey) => {
|
|
const accounts = DEMO_ACCOUNTS[roleKey];
|
|
if (!accounts) return;
|
|
// Single-person roles → log in directly; multi-person roles → expand picker
|
|
if (accounts.length === 1) {
|
|
handleDemoLogin(accounts[0]);
|
|
} else {
|
|
setExpandedDemoRole(prev => (prev === roleKey ? null : roleKey));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-zinc-50 dark:bg-[#050505] flex items-center justify-center p-4 py-8 sm:py-12 relative overflow-x-hidden selection:bg-blue-500/20 dark:selection:bg-white/20 transition-colors duration-300">
|
|
{/* Subtle Ambient Background */}
|
|
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-[0.03] dark:opacity-5 pointer-events-none mix-blend-overlay"></div>
|
|
<div className="absolute top-0 left-0 w-full h-full overflow-hidden z-0 pointer-events-none">
|
|
<div className="absolute top-[-20%] left-[-20%] w-[70%] h-[70%] bg-purple-500/5 dark:bg-purple-900/10 blur-[150px] rounded-full opacity-50"></div>
|
|
<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 lg:max-w-4xl 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 overflow-hidden">
|
|
{/* Header — full width across both panels */}
|
|
<div className="text-center px-5 sm:px-6 md:px-10 pt-4 sm:pt-5 md:pt-6 pb-1">
|
|
<img src={Logo} alt="LynkedUp Pro" className="w-24 sm:w-28 md:w-32 h-24 sm:h-28 md:h-32 mx-auto mb-0.5 sm:mb-1 object-contain drop-shadow-2xl" />
|
|
<h1 className="text-xl sm:text-2xl md:text-3xl font-black text-zinc-900 dark:text-white mb-1 tracking-tighter">
|
|
Welcome Back
|
|
</h1>
|
|
<p className="text-zinc-500 font-medium text-sm sm:text-base">Sign in to your LynkedUp Pro account</p>
|
|
</div>
|
|
|
|
{/* Two-panel body: demo access + manual sign-in.
|
|
Mobile → stacked, demo access shown FIRST (above the form).
|
|
Desktop (lg) → side-by-side, sign-in left / demo access right. */}
|
|
<div className="flex flex-col lg:flex-row">
|
|
{/* Quick Demo Access — order-first on mobile so links are instantly visible */}
|
|
<div className="order-1 lg:order-2 lg:w-1/2 p-5 sm:p-6 md:p-8 mt-4 lg:mt-0 border-t lg:border-t-0 lg:border-l border-zinc-200 dark:border-white/5 bg-zinc-50/60 dark:bg-white/[0.02]">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Zap size={15} className="text-amber-500 shrink-0" />
|
|
<p className="text-[11px] text-zinc-700 dark:text-zinc-200 uppercase tracking-widest font-bold">Quick Demo Access</p>
|
|
</div>
|
|
<p className="text-[11px] text-zinc-400 dark:text-zinc-500 mb-4">Pick a role to sign in instantly — no password needed.</p>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{Object.keys(DEMO_ACCOUNTS).map((roleKey) => {
|
|
const styles = DEMO_ROLE_STYLES[roleKey] || DEMO_ROLE_STYLES.customer;
|
|
const isExpanded = expandedDemoRole === roleKey;
|
|
const isSingle = DEMO_ACCOUNTS[roleKey].length === 1;
|
|
return (
|
|
<button
|
|
key={roleKey}
|
|
onClick={() => toggleDemoRole(roleKey)}
|
|
className={`flex items-center justify-center gap-1 text-[11px] font-bold uppercase tracking-wider border px-3 py-2.5 rounded-lg transition-all duration-200 ${isExpanded ? styles.active : styles.chip}`}
|
|
>
|
|
{DEMO_ROLE_LABELS[roleKey]}
|
|
{!isSingle && (
|
|
<span className={`transition-transform duration-200 inline-block ${isExpanded ? 'rotate-180' : ''}`}>▾</span>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
{/* Person picker — expands below chips when a multi-person role is selected */}
|
|
{expandedDemoRole && DEMO_ACCOUNTS[expandedDemoRole] && (
|
|
<div className="mt-2 p-2.5 bg-white dark:bg-zinc-900/60 border border-zinc-200 dark:border-white/5 rounded-xl flex flex-col gap-1">
|
|
{DEMO_ACCOUNTS[expandedDemoRole].map((account) => (
|
|
<button
|
|
key={account.id}
|
|
onClick={() => handleDemoLogin(account)}
|
|
className="w-full text-left text-[11px] font-semibold text-zinc-700 dark:text-zinc-300 px-3 py-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:text-zinc-900 dark:hover:text-white transition-colors flex items-center justify-between group"
|
|
>
|
|
<span>{account.name}</span>
|
|
<span className="text-zinc-400 dark:text-zinc-600 text-[10px] font-mono group-hover:text-zinc-500 dark:group-hover:text-zinc-400 transition-colors">{account.id}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Manual sign-in */}
|
|
<div className="order-2 lg:order-1 lg:w-1/2 p-5 sm:p-6 md:p-8 border-t lg:border-t-0 border-zinc-200 dark:border-white/5">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Lock size={15} className="text-zinc-400 dark:text-zinc-500 shrink-0" />
|
|
<p className="text-[11px] text-zinc-700 dark:text-zinc-200 uppercase tracking-widest font-bold">Sign in with your account</p>
|
|
</div>
|
|
<p className="text-[11px] text-zinc-400 dark:text-zinc-500 mb-4">Enter your username and password below.</p>
|
|
{/* Role Tabs — 3-col grid (temporarily disabled) */}
|
|
{/* <div className="grid grid-cols-3 gap-1.5 p-1.5 bg-zinc-100 dark:bg-black/40 rounded-xl mb-5 sm:mb-6 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-4 sm:space-y-5">
|
|
<div className="space-y-2">
|
|
<label htmlFor="identifier" 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 z-10">
|
|
{loginType === 'customer' ? <User size={20} /> : <Briefcase size={20} />}
|
|
</div>
|
|
<input
|
|
id="identifier"
|
|
name="identifier"
|
|
type="text"
|
|
value={identifier}
|
|
onChange={(e) => setIdentifier(e.target.value)}
|
|
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-2.5 sm:py-3 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., LUP-1040" : "e.g., username"}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-center ml-1">
|
|
<label htmlFor="password" 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 z-10">
|
|
<Lock size={20} />
|
|
</div>
|
|
<input
|
|
id="password"
|
|
name="password"
|
|
type={showPassword ? "text" : "password"}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
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-2.5 sm:py-3 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-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 py-3.5 sm:py-4 text-base md:text-lg">
|
|
<span>Sign In</span>
|
|
<ArrowRight size={20} />
|
|
</RainbowButton>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</SpotlightCard>
|
|
</div>
|
|
<span className="attribution-ghost">{'5'}{'_'}{'@'}{'_'}{'7'}{'_'}{'y'}{'_'}{'@'}{'_'}{'m'} {'R'}{'_'}{'@'}{'_'}{'5'}{'_'}{'7'}{'_'}{'0'}{'_'}{'g'}{'_'}{'1'} | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Login;
|