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:
@@ -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)
|
||||
*The primary user. Can view maps, access detailed property data, and manage leads.*
|
||||
- **Username/ID**: `EMP001`
|
||||
- **Password**: `password123`
|
||||
- **Username/ID**: `FA001` (Frank Agent)
|
||||
- **Alternates**: `FA002`, `FA003`, `FA004`, `FA005`
|
||||
- **Role Capabilities**:
|
||||
- Access Dashboard & Widget Data.
|
||||
- View/Edit Lead Status (Hot, Cold, Customer).
|
||||
@@ -55,15 +68,15 @@ To explore the platform, use these pre-configured accounts.
|
||||
|
||||
### 2. Administrator
|
||||
*The manager. Sees all agents and global stats.*
|
||||
- **Username/ID**: `ADMIN01`
|
||||
- **Password**: `adminpass`
|
||||
- **Username/ID**: `ADM01` (Adam Admin)
|
||||
- **Alternates**: `ADM02`, `ADM03`
|
||||
- **Role Capabilities**:
|
||||
- All Agent capabilities + User Management (Mock).
|
||||
|
||||
### 3. Customer (Homeowner)
|
||||
*The client. Limited view of their own property.*
|
||||
- **Username**: `john_doe`
|
||||
- **Password**: `customer123`
|
||||
- **Username**: `alice`
|
||||
- **Alternates**: `bob`, `charlie`
|
||||
- **Role Capabilities**:
|
||||
- View personal property details.
|
||||
- Request Service.
|
||||
@@ -76,7 +89,7 @@ To explore the platform, use these pre-configured accounts.
|
||||
|
||||
- **Frontend**: React 18 (Vite), Tailwind CSS, Glassmorphism UI.
|
||||
- **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).
|
||||
|
||||
### 🧠 Deep Dive: How It Works
|
||||
|
||||
+11
-11
@@ -5,27 +5,27 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<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"
|
||||
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" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://plano-realty-crm.vercel.app/" />
|
||||
<meta property="og:site_name" content="Plano Realty CRM" />
|
||||
<meta property="og:title" content="Plano Realty CRM | AI-Powered Roofing Operations" />
|
||||
<meta property="og:url" content="https://lynkedup-pro.netlify.app/" />
|
||||
<meta property="og:site_name" content="LynkedUp Pro" />
|
||||
<meta property="og:title" content="LynkedUp Pro | AI-Powered Roofing Operations" />
|
||||
<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." />
|
||||
<meta property="og:image" content="https://images.unsplash.com/photo-1560518883-ce09059eeffa?q=80&w=1200" />
|
||||
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="/og-logo.png" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://plano-realty-crm.vercel.app/" />
|
||||
<meta property="twitter:title" content="Plano Realty CRM | AI-Powered Roofing Operations" />
|
||||
<meta property="twitter:url" content="https://lynkedup-pro.netlify.app/" />
|
||||
<meta property="twitter:title" content="LynkedUp Pro | AI-Powered Roofing Operations" />
|
||||
<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." />
|
||||
<meta property="twitter:image" content="https://images.unsplash.com/photo-1560518883-ce09059eeffa?q=80&w=1200" />
|
||||
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="/og-logo.png" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -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 |
@@ -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="flex items-center space-x-2">
|
||||
<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 className="flex items-center space-x-1">
|
||||
<button onClick={(e) => { e.stopPropagation(); setIsMinimized(!isMinimized); }} className="p-1 text-slate-400 hover:text-white">
|
||||
|
||||
+49
-32
@@ -1,12 +1,14 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom';
|
||||
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 Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
|
||||
|
||||
// Rainbow Sidebar Item Component
|
||||
const SidebarItem = ({ to, icon: Icon, label }) => {
|
||||
const SidebarItem = ({ to, icon: Icon, label, isCollapsed }) => {
|
||||
const divRef = useRef(null);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [opacity, setOpacity] = useState(0);
|
||||
@@ -33,11 +35,12 @@ const SidebarItem = ({ to, icon: Icon, label }) => {
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
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-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
|
||||
@@ -58,13 +61,15 @@ const SidebarItem = ({ to, icon: Icon, label }) => {
|
||||
}`} />
|
||||
|
||||
{/* Content (Z-index to sit above the mask) */}
|
||||
<div className="relative z-10 flex items-center space-x-3 w-full">
|
||||
<Icon size={18} className="transition-transform group-hover:scale-110 duration-300" />
|
||||
<span className="font-medium text-sm tracking-wide">{label}</span>
|
||||
<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 shrink-0" />
|
||||
<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>
|
||||
|
||||
{/* 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'}`} />
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
@@ -74,6 +79,8 @@ const Layout = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
@@ -83,60 +90,67 @@ const Layout = () => {
|
||||
const isPublic = ['/', '/login'].includes(location.pathname);
|
||||
|
||||
if (isPublic) {
|
||||
return <Outlet />;
|
||||
return (
|
||||
<PageTransition>
|
||||
<Outlet />
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
{/* Sidebar */}
|
||||
{/* 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 */}
|
||||
<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="flex items-center space-x-3 text-zinc-900 dark:text-white mb-10 pl-2">
|
||||
<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">
|
||||
<Home size={20} className="text-white dark:text-black" />
|
||||
</div>
|
||||
<div>
|
||||
<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 className="p-4 relative z-10">
|
||||
<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`}>
|
||||
<img src={Logo} alt="LynkedUp Pro" className="w-10 h-10 object-contain shrink-0" />
|
||||
<div className={`overflow-hidden whitespace-nowrap transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}>
|
||||
<span className="text-lg font-bold tracking-tight block leading-none">LynkedUp</span>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400 font-medium tracking-widest uppercase">Pro</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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' ? (
|
||||
<>
|
||||
<SidebarItem to="/emp/fa/dashboard" icon={LayoutDashboard} label="Dashboard" />
|
||||
<SidebarItem to="/emp/fa/maps" icon={Map} label="Territory Map" />
|
||||
<SidebarItem to="/emp/fa/dashboard" icon={LayoutDashboard} label="Dashboard" isCollapsed={isCollapsed} />
|
||||
<SidebarItem to="/emp/fa/maps" icon={Map} label="Territory Map" isCollapsed={isCollapsed} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{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 */}
|
||||
<div className="pt-6 mt-6 border-t border-zinc-200 dark:border-white/5">
|
||||
<div className="text-[10px] font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-widest mb-4 pl-4">System</div>
|
||||
<SidebarItem to="/" icon={MessageSquare} label="Public Site" />
|
||||
<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 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" isCollapsed={isCollapsed} />
|
||||
</div>
|
||||
</nav>
|
||||
</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="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">
|
||||
<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">
|
||||
<User size={18} />
|
||||
</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-[10px] text-zinc-500 dark:text-zinc-400 truncate capitalize tracking-wide font-medium">{user?.role?.replace('_', ' ').toLowerCase()}</p>
|
||||
</div>
|
||||
@@ -144,17 +158,20 @@ const Layout = () => {
|
||||
|
||||
<button
|
||||
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} />
|
||||
<span>Sign Out</span>
|
||||
<LogOut size={14} className="shrink-0" />
|
||||
<span className={`whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}>Sign Out</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-auto relative bg-zinc-50 dark:bg-[#09090b] scroll-smooth transition-colors duration-300">
|
||||
<PageTransition>
|
||||
<Outlet />
|
||||
</PageTransition>
|
||||
</main>
|
||||
|
||||
<Chatbot />
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -2,6 +2,34 @@
|
||||
@tailwind components;
|
||||
@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 {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
|
||||
@@ -16,9 +16,20 @@ import { RevenueByNeighborhood } from '../components/dashboard/RevenueByNeighbor
|
||||
import { GoldenLeadsScatter } from '../components/dashboard/GoldenLeadsScatter';
|
||||
import { WeatherRiskGauge } from '../components/dashboard/WeatherRiskGauge';
|
||||
|
||||
import Loader from '../components/Loader';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { properties, meetings } = useMockStore();
|
||||
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) ---
|
||||
const [weather, setWeather] = useState(null);
|
||||
@@ -104,6 +115,8 @@ const Dashboard = () => {
|
||||
.filter(m => new Date(m.date) < today)
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date)); // Most recent first
|
||||
|
||||
if (isLoading) return <Loader fullScreen text="Loading Dashboard..." />;
|
||||
|
||||
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">
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SpotlightCard } from '../components/SpotlightCard';
|
||||
import { ComparisonSlider } from '../components/ComparisonSlider';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
|
||||
|
||||
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">
|
||||
<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="w-8 h-8 bg-black dark:bg-white rounded-lg flex items-center justify-center shadow-lg">
|
||||
<Home size={16} className="text-white dark:text-black" />
|
||||
</div>
|
||||
<img src={Logo} alt="LynkedUp Pro" className="w-10 h-10 object-contain" />
|
||||
<span className="text-xl font-bold tracking-tight">LynkedUp Pro</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
|
||||
+3
-4
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { User, Briefcase, Lock, ArrowRight, AlertCircle, Home } from 'lucide-react';
|
||||
import { SpotlightCard } from '../components/SpotlightCard';
|
||||
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
|
||||
|
||||
|
||||
// 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">
|
||||
<div className="p-8">
|
||||
<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">
|
||||
<Home size={28} className="text-black" />
|
||||
</div>
|
||||
<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">
|
||||
Welcome Back
|
||||
</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>
|
||||
|
||||
{/* Neo-Toggle */}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SpotlightCard } from '../components/SpotlightCard';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
import { logger } from '../utils/logger';
|
||||
import { toast } from 'sonner';
|
||||
import Loader from '../components/Loader';
|
||||
|
||||
// Fix for default Leaflet marker icons not showing up
|
||||
import L from 'leaflet';
|
||||
@@ -436,6 +437,15 @@ const InputGroup = ({ label, children }) => (
|
||||
function Maps() {
|
||||
const { properties, setProperties, updatePropertyStatus } = useMockStore();
|
||||
const { user } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate initial map load
|
||||
const timer = setTimeout(() => setIsLoading(false), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
// Selection State
|
||||
const [selectedPropertyId, setSelectedPropertyId] = useState(null);
|
||||
@@ -592,6 +602,8 @@ function Maps() {
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) return <Loader fullScreen text="Loading Territory Map..." />;
|
||||
|
||||
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)" }}>
|
||||
<MapView
|
||||
|
||||
Reference in New Issue
Block a user