diff --git a/src/components/BackToTopButton.tsx b/src/components/BackToTopButton.tsx new file mode 100644 index 0000000..0e97023 --- /dev/null +++ b/src/components/BackToTopButton.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from "react"; +import { ArrowUp } from "lucide-react"; + +export default function BackToTopButton() { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const toggleVisibility = () => { + const main = document.querySelector("main"); + + const scrollTop = + window.scrollY || + document.documentElement.scrollTop || + document.body.scrollTop || + main?.scrollTop || + 0; + + setIsVisible(scrollTop > 300); + }; + + window.addEventListener("scroll", toggleVisibility); + toggleVisibility(); + + return () => window.removeEventListener("scroll", toggleVisibility); + }, []); + + const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }; + return ( + + ); +} diff --git a/src/components/shared/ScanSkeleton.tsx b/src/components/shared/ScanSkeleton.tsx new file mode 100644 index 0000000..60559fd --- /dev/null +++ b/src/components/shared/ScanSkeleton.tsx @@ -0,0 +1,33 @@ +import GlassCard from "../GlassCard"; +export default function ScanSkeleton() { + return ( +
+
+ +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index e1d1223..055a6c0 100644 --- a/src/index.css +++ b/src/index.css @@ -396,4 +396,25 @@ input::placeholder { @media print { nav, .print\:hidden { display: none !important; } +} +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + + 100% { + background-position: 200% 0; + } +} + +.skeleton-shimmer { + background: linear-gradient( + 90deg, + var(--color-surface-mid) 25%, + var(--color-surface-highest) 50%, + var(--color-surface-mid) 75% + ); + + background-size: 200% 100%; + animation: shimmer 1.5s linear infinite; } \ No newline at end of file diff --git a/src/pages/AnalysisDashboard.tsx b/src/pages/AnalysisDashboard.tsx index b5c2925..14d4f89 100644 --- a/src/pages/AnalysisDashboard.tsx +++ b/src/pages/AnalysisDashboard.tsx @@ -5,6 +5,7 @@ import GlassCard from '../components/GlassCard'; import StatusTerminal from '../components/StatusTerminal'; import { api } from '../lib/api'; import type { ScanResult } from '../lib/types'; +import ScanSkeleton from "../components/shared/ScanSkeleton"; const BIOMARKER_META = { gill_saturation: { label: 'Gill Saturation', icon: Droplets }, corneal_clarity: { label: 'Corneal Clarity', icon: EyeIcon }, @@ -69,13 +70,9 @@ export default function AnalysisDashboard() { load(); }, [params]); - - if (loading) { - return ( -
- -
- ); + // ── Loading state ──────────────────────────────────────────────────────── + if (loading) { + return ; } // ── Error state ────────────────────────────────────────────────────────── diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index 8799eab..f4bdf2b 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -3,6 +3,7 @@ import { isAuthenticated } from '../lib/api'; import { Zap, Eye, MapPin, ScanLine, Target, Award, ChevronDown } from 'lucide-react'; import GlassCard from '../components/GlassCard'; import { useState, useEffect } from 'react'; +import BackToTopButton from '../components/BackToTopButton'; const features = [ { @@ -77,7 +78,7 @@ export default function LandingPage() { }, []); return ( -
+
{/* ── HERO ── */}
@@ -279,6 +280,7 @@ export default function LandingPage() {
+
); }