+
-
+
{user?.name}
{user?.role?.replace('_', ' ').toLowerCase()}
@@ -144,17 +158,20 @@ const Layout = () => {
{/* Main Content */}
-
+
+
+
diff --git a/src/components/Loader.jsx b/src/components/Loader.jsx
new file mode 100644
index 0000000..073c682
--- /dev/null
+++ b/src/components/Loader.jsx
@@ -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 = (
+
+
+ {/* Pulsing Ripple Effect */}
+
+
+

+
+
+ {text && (
+
+ )}
+
+ );
+
+ if (fullScreen) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return
{content}
;
+};
+
+export default Loader;
diff --git a/src/components/PageTransition.jsx b/src/components/PageTransition.jsx
new file mode 100644
index 0000000..c7538e7
--- /dev/null
+++ b/src/components/PageTransition.jsx
@@ -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 (
+
+ {children}
+
+ );
+};
+
+export default PageTransition;
diff --git a/src/index.css b/src/index.css
index 94d86a6..11a1e53 100644
--- a/src/index.css
+++ b/src/index.css
@@ -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;
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index 072cbdd..d122a1e 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -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
;
+
return (
diff --git a/src/pages/Landing.jsx b/src/pages/Landing.jsx
index 123588f..bb9bd67 100644
--- a/src/pages/Landing.jsx
+++ b/src/pages/Landing.jsx
@@ -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,10 +57,8 @@ const Landing = () => {