feat: Enhance UX with animations, loaders, fixes, and doc updates

- Animations: Added global page transitions and smooth sidebar toggle
- Loaders: Implemented branded Loader component and page loading states
- Fixes: Resolved hook order violations in Maps/Dashboard; fixed Layout import
- Docs: Updated README with correct demo credentials and feature list
This commit is contained in:
Satyam
2026-02-01 18:48:47 +05:30
parent 719172372e
commit 96f93d0c93
14 changed files with 209 additions and 63 deletions
+22 -9
View File
@@ -40,14 +40,27 @@ A 360-degree view of your territory's health.
--- ---
## 👥 USER ROLES & DEMO ACCOUNTS ## KEY FEATURES (Latest Updates)
To explore the platform, use these pre-configured accounts. ### 🎨 "LynkedUp Pro" Branding & UX
- **Smooth Animations**: Global page transitions (Fade/Slide) and silky-smooth collapsible sidebar.
- **Visual Polish**: Glassmorphism everywhere, custom "LynkedUp" loaders, and pulsing interactive elements.
- **Dark Mode**: Fully supported system-wide dark mode with inverted map tiles for comfortable night viewing.
### 🌍 Advanced Mapping
- **Lead Creation**: Click anywhere on the map to reverse-geocode addresses and create new leads on the fly.
- **Polygon Visualization**: Color-coded property footprints indicating status (Hot Lead, Customer, etc.).
---
## 👥 USER ROLES & DEMO ACCOUNTS
To explore the platform, use these pre-configured accounts. **Password for ALL accounts is: `password`**
### 1. Field Agent (Worker) ### 1. Field Agent (Worker)
*The primary user. Can view maps, access detailed property data, and manage leads.* *The primary user. Can view maps, access detailed property data, and manage leads.*
- **Username/ID**: `EMP001` - **Username/ID**: `FA001` (Frank Agent)
- **Password**: `password123` - **Alternates**: `FA002`, `FA003`, `FA004`, `FA005`
- **Role Capabilities**: - **Role Capabilities**:
- Access Dashboard & Widget Data. - Access Dashboard & Widget Data.
- View/Edit Lead Status (Hot, Cold, Customer). - View/Edit Lead Status (Hot, Cold, Customer).
@@ -55,15 +68,15 @@ To explore the platform, use these pre-configured accounts.
### 2. Administrator ### 2. Administrator
*The manager. Sees all agents and global stats.* *The manager. Sees all agents and global stats.*
- **Username/ID**: `ADMIN01` - **Username/ID**: `ADM01` (Adam Admin)
- **Password**: `adminpass` - **Alternates**: `ADM02`, `ADM03`
- **Role Capabilities**: - **Role Capabilities**:
- All Agent capabilities + User Management (Mock). - All Agent capabilities + User Management (Mock).
### 3. Customer (Homeowner) ### 3. Customer (Homeowner)
*The client. Limited view of their own property.* *The client. Limited view of their own property.*
- **Username**: `john_doe` - **Username**: `alice`
- **Password**: `customer123` - **Alternates**: `bob`, `charlie`
- **Role Capabilities**: - **Role Capabilities**:
- View personal property details. - View personal property details.
- Request Service. - Request Service.
@@ -76,7 +89,7 @@ To explore the platform, use these pre-configured accounts.
- **Frontend**: React 18 (Vite), Tailwind CSS, Glassmorphism UI. - **Frontend**: React 18 (Vite), Tailwind CSS, Glassmorphism UI.
- **State**: `MockStore` (Simulates Backend/DB), `AuthContext` (Session Management). - **State**: `MockStore` (Simulates Backend/DB), `AuthContext` (Session Management).
- **AI Engine**: Groq SDK (`qwen-2.5-32b`) injected with dynamic system prompts. - **AI Engine**: Groq SDK (`qwen-3-32b`) injected with dynamic system prompts.
- **Visuals**: `react-leaflet` (Maps), `recharts` (Analytics), `lucide-react` (Icons). - **Visuals**: `react-leaflet` (Maps), `recharts` (Analytics), `lucide-react` (Icons).
### 🧠 Deep Dive: How It Works ### 🧠 Deep Dive: How It Works
+11 -11
View File
@@ -5,27 +5,27 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Plano Realty CRM</title> <title>LynkedUp Pro - Next Gen Roofing CRM</title>
<meta name="description" <meta name="description"
content="The all-in-one geospatial platform for Plano roofing agents. Features real-time mapping, AI scheduling concierge, and instant property insights." /> content="The all-in-one geospatial platform for roofing professionals. Features real-time mapping, AI diagnoses, and instant estimates using LiDAR technology." />
<meta name="theme-color" content="#09090b" /> <meta name="theme-color" content="#09090b" />
<!-- Open Graph / Facebook --> <!-- Open Graph / Facebook -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="https://plano-realty-crm.vercel.app/" /> <meta property="og:url" content="https://lynkedup-pro.netlify.app/" />
<meta property="og:site_name" content="Plano Realty CRM" /> <meta property="og:site_name" content="LynkedUp Pro" />
<meta property="og:title" content="Plano Realty CRM | AI-Powered Roofing Operations" /> <meta property="og:title" content="LynkedUp Pro | AI-Powered Roofing Operations" />
<meta property="og:description" <meta property="og:description"
content="The all-in-one geospatial platform for Plano roofing agents. Features real-time mapping, AI scheduling concierge, and instant property insights." /> content="The all-in-one geospatial platform for roofing professionals. Features real-time mapping, AI diagnoses, and instant estimates using LiDAR technology." />
<meta property="og:image" content="https://images.unsplash.com/photo-1560518883-ce09059eeffa?q=80&w=1200" /> <meta property="og:image" content="/og-logo.png" />
<!-- Twitter --> <!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://plano-realty-crm.vercel.app/" /> <meta property="twitter:url" content="https://lynkedup-pro.netlify.app/" />
<meta property="twitter:title" content="Plano Realty CRM | AI-Powered Roofing Operations" /> <meta property="twitter:title" content="LynkedUp Pro | AI-Powered Roofing Operations" />
<meta property="twitter:description" <meta property="twitter:description"
content="The all-in-one geospatial platform for Plano roofing agents. Features real-time mapping, AI scheduling concierge, and instant property insights." /> content="The all-in-one geospatial platform for roofing professionals. Features real-time mapping, AI diagnoses, and instant estimates using LiDAR technology." />
<meta property="twitter:image" content="https://images.unsplash.com/photo-1560518883-ce09059eeffa?q=80&w=1200" /> <meta property="twitter:image" content="/og-logo.png" />
</head> </head>
<body> <body>
+4
View File
@@ -0,0 +1,4 @@
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

+1 -1
View File
@@ -259,7 +259,7 @@ ${scheduleSummary}`;
<div className="bg-slate-900 dark:bg-black p-4 flex items-center justify-between shrink-0 cursor-pointer" onClick={() => !isMinimized && setIsMinimized(!isMinimized)}> <div className="bg-slate-900 dark:bg-black p-4 flex items-center justify-between shrink-0 cursor-pointer" onClick={() => !isMinimized && setIsMinimized(!isMinimized)}>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div> <div className="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div>
<h3 className="text-white font-bold text-sm">Plano AI Assistant</h3> <h3 className="text-white font-bold text-sm">LynkedUp AI</h3>
</div> </div>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<button onClick={(e) => { e.stopPropagation(); setIsMinimized(!isMinimized); }} className="p-1 text-slate-400 hover:text-white"> <button onClick={(e) => { e.stopPropagation(); setIsMinimized(!isMinimized); }} className="p-1 text-slate-400 hover:text-white">
+49 -32
View File
@@ -1,12 +1,14 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { LayoutDashboard, Map, Calendar, LogOut, User, Home, MessageSquare } from 'lucide-react'; import { LayoutDashboard, Map, Calendar, LogOut, User, Home, MessageSquare, ChevronLeft, ChevronRight } from 'lucide-react';
import PageTransition from './PageTransition';
import Chatbot from './Chatbot'; import Chatbot from './Chatbot';
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
// Rainbow Sidebar Item Component // Rainbow Sidebar Item Component
const SidebarItem = ({ to, icon: Icon, label }) => { const SidebarItem = ({ to, icon: Icon, label, isCollapsed }) => {
const divRef = useRef(null); const divRef = useRef(null);
const [position, setPosition] = useState({ x: 0, y: 0 }); const [position, setPosition] = useState({ x: 0, y: 0 });
const [opacity, setOpacity] = useState(0); const [opacity, setOpacity] = useState(0);
@@ -33,11 +35,12 @@ const SidebarItem = ({ to, icon: Icon, label }) => {
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
className={({ isActive }) => className={({ isActive }) =>
`relative flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-300 group overflow-hidden mb-1 ${isActive `relative flex items-center ${isCollapsed ? 'justify-center' : 'space-x-3'} px-4 py-3 rounded-xl transition-all duration-300 group overflow-hidden mb-1 ${isActive
? 'text-zinc-900 dark:text-white bg-black/5 dark:bg-white/5 shadow-sm dark:shadow-black/10' ? 'text-zinc-900 dark:text-white bg-black/5 dark:bg-white/5 shadow-sm dark:shadow-black/10'
: 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white bg-transparent hover:bg-black/5 dark:hover:bg-white/5' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white bg-transparent hover:bg-black/5 dark:hover:bg-white/5'
}` }`
} }
title={isCollapsed ? label : ""}
> >
{/* {/*
Rainbow Border Layer Rainbow Border Layer
@@ -58,13 +61,15 @@ const SidebarItem = ({ to, icon: Icon, label }) => {
}`} /> }`} />
{/* Content (Z-index to sit above the mask) */} {/* Content (Z-index to sit above the mask) */}
<div className="relative z-10 flex items-center space-x-3 w-full"> <div className={`relative z-10 flex items-center ${isCollapsed ? 'justify-center' : 'space-x-3'} w-full`}>
<Icon size={18} className="transition-transform group-hover:scale-110 duration-300" /> <Icon size={18} className="transition-transform group-hover:scale-110 duration-300 shrink-0" />
<span className="font-medium text-sm tracking-wide">{label}</span> <span className={`font-medium text-sm tracking-wide whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100 ml-3'}`}>{label}</span>
</div> </div>
{/* Active Indicator Dot */} {/* Active Indicator Dot */}
{!isCollapsed && (
<div className={({ isActive }) => `absolute right-2 w-1.5 h-1.5 rounded-full bg-zinc-900 dark:bg-white transition-all duration-300 ${isActive ? 'opacity-100 scale-100' : 'opacity-0 scale-0'}`} /> <div className={({ isActive }) => `absolute right-2 w-1.5 h-1.5 rounded-full bg-zinc-900 dark:bg-white transition-all duration-300 ${isActive ? 'opacity-100 scale-100' : 'opacity-0 scale-0'}`} />
)}
</NavLink> </NavLink>
); );
}; };
@@ -74,6 +79,8 @@ const Layout = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [isCollapsed, setIsCollapsed] = useState(false);
const handleLogout = () => { const handleLogout = () => {
logout(); logout();
navigate('/login'); navigate('/login');
@@ -83,60 +90,67 @@ const Layout = () => {
const isPublic = ['/', '/login'].includes(location.pathname); const isPublic = ['/', '/login'].includes(location.pathname);
if (isPublic) { if (isPublic) {
return <Outlet />; return (
<PageTransition>
<Outlet />
</PageTransition>
);
} }
return ( return (
<div className="flex h-screen bg-zinc-50 dark:bg-[#050505] text-zinc-900 dark:text-white overflow-hidden font-sans selection:bg-blue-500/20 dark:selection:bg-white/20 transition-colors duration-300"> <div className="flex h-screen bg-zinc-50 dark:bg-[#050505] text-zinc-900 dark:text-white overflow-hidden font-sans selection:bg-blue-500/20 dark:selection:bg-white/20 transition-colors duration-300">
{/* Sidebar */} {/* Sidebar */}
{/* Enhanced glassmorphism with light/dark support */} {/* Enhanced glassmorphism with light/dark support */}
<aside className="w-72 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-2xl border-r border-zinc-200 dark:border-white/5 flex flex-col shrink-0 z-30 shadow-[4px_0_24px_rgba(0,0,0,0.05)] dark:shadow-[4px_0_24px_rgba(0,0,0,0.4)] relative"> <aside className={`${isCollapsed ? 'w-20' : 'w-72'} bg-white/60 dark:bg-zinc-900/60 backdrop-blur-2xl border-r border-zinc-200 dark:border-white/5 flex flex-col shrink-0 z-30 shadow-[4px_0_24px_rgba(0,0,0,0.05)] dark:shadow-[4px_0_24px_rgba(0,0,0,0.4)] relative transition-all duration-300 ease-in-out`}>
{/* Collapse Toggle Button */}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="absolute -right-3 top-9 w-6 h-6 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 rounded-full flex items-center justify-center text-zinc-500 hover:text-zinc-900 dark:hover:text-white shadow-sm z-50 transition-colors"
>
{isCollapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
</button>
{/* Noise texture */} {/* Noise texture */}
<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 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="p-6 relative z-10"> <div className="p-4 relative z-10">
<div className="flex items-center space-x-3 text-zinc-900 dark:text-white mb-10 pl-2"> <div className={`flex items-center ${isCollapsed ? 'justify-center' : 'space-x-3'} text-zinc-900 dark:text-white mb-10 pl-2 transition-all duration-300`}>
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-zinc-800 to-black dark:from-white dark:to-zinc-400 flex items-center justify-center shadow-lg shadow-black/10 dark:shadow-white/10 ring-1 ring-black/5 dark:ring-white/20"> <img src={Logo} alt="LynkedUp Pro" className="w-10 h-10 object-contain shrink-0" />
<Home size={20} className="text-white dark:text-black" /> <div className={`overflow-hidden whitespace-nowrap transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}>
</div> <span className="text-lg font-bold tracking-tight block leading-none">LynkedUp</span>
<div> <span className="text-xs text-zinc-500 dark:text-zinc-400 font-medium tracking-widest uppercase">Pro</span>
<span className="text-lg font-bold tracking-tight block leading-none">Plano</span>
<span className="text-xs text-zinc-500 dark:text-zinc-400 font-medium tracking-widest uppercase">Realty</span>
</div> </div>
</div> </div>
<nav className="space-y-1"> <nav className="space-y-1">
<div className="text-[10px] font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-widest mb-4 pl-4 pt-2">Menu</div> <div className={`text-[10px] font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-widest mb-4 pl-4 pt-2 whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'opacity-0 h-0 scale-y-0' : 'opacity-100 h-auto scale-y-100'}`}>Menu</div>
{user?.role === 'FIELD_AGENT' || user?.role === 'ADMIN' ? ( {user?.role === 'FIELD_AGENT' || user?.role === 'ADMIN' ? (
<> <>
<SidebarItem to="/emp/fa/dashboard" icon={LayoutDashboard} label="Dashboard" /> <SidebarItem to="/emp/fa/dashboard" icon={LayoutDashboard} label="Dashboard" isCollapsed={isCollapsed} />
<SidebarItem to="/emp/fa/maps" icon={Map} label="Territory Map" /> <SidebarItem to="/emp/fa/maps" icon={Map} label="Territory Map" isCollapsed={isCollapsed} />
</> </>
) : null} ) : null}
{user?.role === 'ADMIN' && ( {user?.role === 'ADMIN' && (
<SidebarItem to="/admin/schedule" icon={Calendar} label="Team Schedule" /> <SidebarItem to="/admin/schedule" icon={Calendar} label="Team Schedule" isCollapsed={isCollapsed} />
)} )}
{/* Common Links */} {/* Common Links */}
<div className="pt-6 mt-6 border-t border-zinc-200 dark:border-white/5"> <div className={`pt-6 mt-6 border-t border-zinc-200 dark:border-white/5 transition-all duration-300 ${isCollapsed ? 'flex justify-center' : ''}`}>
<div className="text-[10px] font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-widest mb-4 pl-4">System</div> <div className={`text-[10px] font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-widest mb-4 pl-4 whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'opacity-0 h-0 scale-y-0' : 'opacity-100 h-auto scale-y-100'}`}>System</div>
<SidebarItem to="/" icon={MessageSquare} label="Public Site" /> <SidebarItem to="/" icon={MessageSquare} label="Public Site" isCollapsed={isCollapsed} />
</div> </div>
</nav> </nav>
</div> </div>
<div className="mt-auto p-4 border-t border-zinc-200 dark:border-white/5 bg-zinc-50/50 dark:bg-black/20 relative z-10"> <div className="mt-auto p-4 border-t border-zinc-200 dark:border-white/5 bg-zinc-50/50 dark:bg-black/20 relative z-10">
<div className={`flex items-center ${isCollapsed ? 'justify-center' : 'space-x-3'} p-3 mb-3 rounded-xl bg-white/40 dark:bg-white/5 border border-zinc-200 dark:border-white/5 backdrop-blur-md transition-all duration-300 hover:bg-white/60 dark:hover:bg-white/10 group cursor-default`}>
<div className="w-10 h-10 rounded-full bg-zinc-200 dark:bg-zinc-800 flex items-center justify-center text-zinc-600 dark:text-zinc-400 border border-zinc-300 dark:border-white/5 group-hover:border-zinc-400 dark:group-hover:border-white/20 transition-colors shrink-0">
<div className="flex items-center space-x-3 p-3 mb-3 rounded-xl bg-white/40 dark:bg-white/5 border border-zinc-200 dark:border-white/5 backdrop-blur-md transition-colors hover:bg-white/60 dark:hover:bg-white/10 group cursor-default">
<div className="w-10 h-10 rounded-full bg-zinc-200 dark:bg-zinc-800 flex items-center justify-center text-zinc-600 dark:text-zinc-400 border border-zinc-300 dark:border-white/5 group-hover:border-zinc-400 dark:group-hover:border-white/20 transition-colors">
<User size={18} /> <User size={18} />
</div> </div>
<div className="flex-1 min-w-0"> <div className={`flex-1 min-w-0 overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}>
<p className="text-sm font-bold text-zinc-900 dark:text-white truncate">{user?.name}</p> <p className="text-sm font-bold text-zinc-900 dark:text-white truncate">{user?.name}</p>
<p className="text-[10px] text-zinc-500 dark:text-zinc-400 truncate capitalize tracking-wide font-medium">{user?.role?.replace('_', ' ').toLowerCase()}</p> <p className="text-[10px] text-zinc-500 dark:text-zinc-400 truncate capitalize tracking-wide font-medium">{user?.role?.replace('_', ' ').toLowerCase()}</p>
</div> </div>
@@ -144,17 +158,20 @@ const Layout = () => {
<button <button
onClick={handleLogout} onClick={handleLogout}
className="flex items-center justify-center space-x-2 w-full px-4 py-3 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-500/10 rounded-xl transition-all border border-transparent hover:border-red-500/20" className={`flex items-center ${isCollapsed ? 'justify-center' : 'space-x-2'} w-full px-4 py-3 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-500/10 rounded-xl transition-all border border-transparent hover:border-red-500/20`}
title={isCollapsed ? "Sign Out" : ""}
> >
<LogOut size={14} /> <LogOut size={14} className="shrink-0" />
<span>Sign Out</span> <span className={`whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}>Sign Out</span>
</button> </button>
</div> </div>
</aside> </aside>
{/* Main Content */} {/* Main Content */}
<main className="flex-1 overflow-auto relative bg-zinc-50 dark:bg-[#09090b] scroll-smooth transition-colors duration-300"> <main className="flex-1 overflow-auto relative bg-zinc-50 dark:bg-[#09090b] scroll-smooth transition-colors duration-300">
<PageTransition>
<Outlet /> <Outlet />
</PageTransition>
</main> </main>
<Chatbot /> <Chatbot />
+38
View File
@@ -0,0 +1,38 @@
import React from 'react';
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
const Loader = ({ fullScreen = false, text = "Loading..." }) => {
const content = (
<div className="flex flex-col items-center justify-center space-y-4">
<div className="relative">
{/* Pulsing Ripple Effect */}
<div className="absolute inset-0 bg-blue-500/20 rounded-full animate-ping"></div>
<div className="relative bg-white dark:bg-black p-3 rounded-2xl shadow-xl shadow-blue-500/10 border border-zinc-100 dark:border-white/10 z-10">
<img src={Logo} alt="Loading..." className="w-12 h-12 object-contain animate-pulse" />
</div>
</div>
{text && (
<div className="flex flex-col items-center">
<p className="text-xs font-bold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 animate-pulse">{text}</p>
<div className="flex space-x-1 mt-2">
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce [animation-delay:-0.3s]"></div>
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce [animation-delay:-0.15s]"></div>
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce"></div>
</div>
</div>
)}
</div>
);
if (fullScreen) {
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-zinc-50/80 dark:bg-black/80 backdrop-blur-md transition-all duration-500">
{content}
</div>
);
}
return <div className="flex items-center justify-center p-8">{content}</div>;
};
export default Loader;
+23
View File
@@ -0,0 +1,23 @@
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
const PageTransition = ({ children }) => {
const location = useLocation();
const [displayLocation, setDisplayLocation] = useState(location);
const [transitionStage, setTransitionStage] = useState('fadeIn');
// Simple Fade In animation on mount/route change
// Note: For complex exit animations, we'd need a more robust library like framer-motion.
// Given the constraints and desire for "speed", a quick fade-in is often best.
return (
<div
key={location.pathname}
className="animate-in fade-in slide-in-from-bottom-4 duration-500 ease-out fill-mode-forwards"
>
{children}
</div>
);
};
export default PageTransition;
+28
View File
@@ -2,6 +2,34 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer utilities {
.animate-in {
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
animation-fill-mode: forwards;
}
.fade-in {
opacity: 0;
animation-name: fadeIn;
}
.slide-in-from-bottom-4 {
transform: translateY(1rem);
animation-name: slideInBottom;
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideInBottom {
from { transform: translateY(1rem); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
:root { :root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5; line-height: 1.5;
+13
View File
@@ -16,9 +16,20 @@ import { RevenueByNeighborhood } from '../components/dashboard/RevenueByNeighbor
import { GoldenLeadsScatter } from '../components/dashboard/GoldenLeadsScatter'; import { GoldenLeadsScatter } from '../components/dashboard/GoldenLeadsScatter';
import { WeatherRiskGauge } from '../components/dashboard/WeatherRiskGauge'; import { WeatherRiskGauge } from '../components/dashboard/WeatherRiskGauge';
import Loader from '../components/Loader';
const Dashboard = () => { const Dashboard = () => {
const { properties, meetings } = useMockStore(); const { properties, meetings } = useMockStore();
const { user } = useAuth(); const { user } = useAuth();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Simulate initial data fetch
const timer = setTimeout(() => setIsLoading(false), 800);
return () => clearTimeout(timer);
}, []);
// --- Weather State (Lifted) --- // --- Weather State (Lifted) ---
const [weather, setWeather] = useState(null); const [weather, setWeather] = useState(null);
@@ -104,6 +115,8 @@ const Dashboard = () => {
.filter(m => new Date(m.date) < today) .filter(m => new Date(m.date) < today)
.sort((a, b) => new Date(b.date) - new Date(a.date)); // Most recent first .sort((a, b) => new Date(b.date) - new Date(a.date)); // Most recent first
if (isLoading) return <Loader fullScreen text="Loading Dashboard..." />;
return ( return (
<div className="min-h-full bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white selection:bg-blue-500/30 relative pb-20 transition-colors duration-300"> <div className="min-h-full bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white selection:bg-blue-500/30 relative pb-20 transition-colors duration-300">
+2 -3
View File
@@ -8,6 +8,7 @@ import { SpotlightCard } from '../components/SpotlightCard';
import { ComparisonSlider } from '../components/ComparisonSlider'; import { ComparisonSlider } from '../components/ComparisonSlider';
import gsap from 'gsap'; import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { ScrollTrigger } from 'gsap/ScrollTrigger';
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
@@ -56,9 +57,7 @@ const Landing = () => {
<nav className="border-b border-zinc-200 dark:border-white/5 backdrop-blur-xl sticky top-0 z-40 bg-white/70 dark:bg-black/50"> <nav className="border-b border-zinc-200 dark:border-white/5 backdrop-blur-xl sticky top-0 z-40 bg-white/70 dark:bg-black/50">
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between"> <div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-black dark:bg-white rounded-lg flex items-center justify-center shadow-lg"> <img src={Logo} alt="LynkedUp Pro" className="w-10 h-10 object-contain" />
<Home size={16} className="text-white dark:text-black" />
</div>
<span className="text-xl font-bold tracking-tight">LynkedUp Pro</span> <span className="text-xl font-bold tracking-tight">LynkedUp Pro</span>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
+3 -4
View File
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { User, Briefcase, Lock, ArrowRight, AlertCircle, Home } from 'lucide-react'; import { User, Briefcase, Lock, ArrowRight, AlertCircle, Home } from 'lucide-react';
import { SpotlightCard } from '../components/SpotlightCard'; import { SpotlightCard } from '../components/SpotlightCard';
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
// Rainbow Button Component // Rainbow Button Component
@@ -114,13 +115,11 @@ const Login = () => {
<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"> <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="p-8">
<div className="text-center mb-10 pt-2"> <div className="text-center mb-10 pt-2">
<div className="w-14 h-14 bg-gradient-to-br from-zinc-100 to-zinc-300 dark:from-white dark:to-zinc-400 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-xl shadow-black/5 dark:shadow-white/5 ring-1 ring-black/5 dark:ring-white/20"> <img src={Logo} alt="LynkedUp Pro" className="w-20 h-20 mx-auto mb-6 object-contain drop-shadow-2xl" />
<Home size={28} className="text-black" />
</div>
<h1 className="text-3xl font-black text-zinc-900 dark:text-white mb-2 tracking-tighter"> <h1 className="text-3xl font-black text-zinc-900 dark:text-white mb-2 tracking-tighter">
Welcome Back Welcome Back
</h1> </h1>
<p className="text-zinc-500 font-medium">Sign in to your Plano Realty account</p> <p className="text-zinc-500 font-medium">Sign in to your LynkedUp Pro account</p>
</div> </div>
{/* Neo-Toggle */} {/* Neo-Toggle */}
+12
View File
@@ -8,6 +8,7 @@ import { SpotlightCard } from '../components/SpotlightCard';
import { useTheme } from '../context/ThemeContext'; import { useTheme } from '../context/ThemeContext';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { toast } from 'sonner'; import { toast } from 'sonner';
import Loader from '../components/Loader';
// Fix for default Leaflet marker icons not showing up // Fix for default Leaflet marker icons not showing up
import L from 'leaflet'; import L from 'leaflet';
@@ -436,6 +437,15 @@ const InputGroup = ({ label, children }) => (
function Maps() { function Maps() {
const { properties, setProperties, updatePropertyStatus } = useMockStore(); const { properties, setProperties, updatePropertyStatus } = useMockStore();
const { user } = useAuth(); const { user } = useAuth();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Simulate initial map load
const timer = setTimeout(() => setIsLoading(false), 1000);
return () => clearTimeout(timer);
}, []);
// Selection State // Selection State
const [selectedPropertyId, setSelectedPropertyId] = useState(null); const [selectedPropertyId, setSelectedPropertyId] = useState(null);
@@ -592,6 +602,8 @@ function Maps() {
} }
}; };
if (isLoading) return <Loader fullScreen text="Loading Territory Map..." />;
return ( return (
<div className="w-full h-full relative bg-zinc-50 dark:bg-black overflow-hidden transition-colors duration-300" style={{ height: "calc(100vh - 0px)" }}> <div className="w-full h-full relative bg-zinc-50 dark:bg-black overflow-hidden transition-colors duration-300" style={{ height: "calc(100vh - 0px)" }}>
<MapView <MapView