From 2f5b363e5d1cf5d02b963f4071f2eae0b1810b79 Mon Sep 17 00:00:00 2001 From: Volodymyr Ajentik Date: Tue, 17 Mar 2026 05:59:41 +0800 Subject: [PATCH] feat: migrate icon system from lucide-react to doo-iconik Replace all lucide-react icons (44 unique across 19 files) and 12 inline SVGs with the doo-iconik hand-drawn icon set via a typed bridge module. - Add src/lib/icons/ bridge: DooIcon component + 60-icon data subset extracted from ajentik/doo-iconik (MIT) - Migrate 19 component files to use - Remove lucide-react dependency (-577KB from bundle) - Update constitution and README to reflect new icon convention - Fix a11y: DooIcon defaults to aria-hidden="true" for decorative icons - Zero lint errors, zero type errors, build passes clean --- .specify/memory/constitution.md | 2 +- README.md | 2 +- package-lock.json | 10 - package.json | 1 - src/components/TranscriptionConfirmation.tsx | 6 +- src/components/aac/AACResultCard.tsx | 10 +- src/components/aac/AACSearchPanel.tsx | 12 +- src/components/chat/ChatInput.tsx | 6 +- src/components/chat/ChatPanel.tsx | 8 +- src/components/chat/ToolResultCard.tsx | 24 +- src/components/chat/VoiceButton.tsx | 4 +- src/components/events/EventCard.tsx | 45 +- src/components/events/EventFilter.tsx | 6 +- src/components/events/EventsPanel.tsx | 10 +- src/components/layout/AppShell.tsx | 149 ++--- src/components/layout/HeroIntro.tsx | 35 +- src/components/layout/MobileSheet.tsx | 4 +- src/components/layout/Onboarding.tsx | 25 +- src/components/map/AerialViewButton.tsx | 12 +- src/components/map/EventPopup.tsx | 76 +-- src/components/map/POIPopup.tsx | 99 +--- src/components/map/RouteOverlay.tsx | 10 +- src/components/map/StreetViewPanel.tsx | 65 +-- .../HealthNavigationSuggestions.tsx | 28 +- .../navigation/NavigationChatPanel.tsx | 31 +- src/components/ui/AACEventsSection.tsx | 4 +- src/components/ui/EventRow.tsx | 10 +- src/components/ui/POICard.tsx | 56 +- src/components/ui/VenueCard.tsx | 86 +-- src/components/ui/empty-state.tsx | 4 +- src/components/ui/error-state.tsx | 8 +- src/components/ui/select.tsx | 12 +- src/components/ui/sonner.tsx | 12 +- .../voice/TranscriptionFeedback.tsx | 20 +- src/lib/event-icons.ts | 43 +- src/lib/icons/DooIcon.tsx | 58 ++ src/lib/icons/icon-data.ts | 517 ++++++++++++++++++ src/lib/icons/index.ts | 3 + tests/lib/event-icons.test.ts | 46 +- 39 files changed, 905 insertions(+), 654 deletions(-) create mode 100644 src/lib/icons/DooIcon.tsx create mode 100644 src/lib/icons/icon-data.ts create mode 100644 src/lib/icons/index.ts diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 0b01e54..7df1abe 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -48,7 +48,7 @@ The landing page MUST load within 3 seconds on standard broadband. Transitions b - **Maps**: `@vis.gl/react-google-maps` for all Google Maps integration. `APIProvider` MUST wrap the map tree. Raw `gmp-*` web components are acceptable ONLY for elements without React bindings (e.g., `gmp-polyline-3d`). - **AI**: Vercel AI SDK with Google Gemini. Tool definitions in `src/lib/ai/tools.ts`. - **Voice**: Web Speech API wrappers in `src/lib/voice/`. -- **Icons**: Inline SVG or Lucide React. No icon font libraries. +- **Icons**: doo-iconik bridge (`src/lib/icons/`) using ``. Icon data sourced from [ajentik/doo-iconik](https://github.com/ajentik/doo-iconik) (MIT). No icon font libraries. - **Deploy**: Railway auto-deploy from `main` branch. GitHub Actions CI runs lint + build on all PRs. ## Development Workflow diff --git a/README.md b/README.md index 52cd413..bc453ff 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,7 @@ The project follows five core principles that govern all development decisions: - **Styling**: Tailwind CSS 4 with OKLCH color tokens in `globals.css`. No inline style objects except for dynamic values. - **State**: Zustand 5 as the single global store at `src/store/app-store.ts`. No prop drilling beyond one level. - **Maps**: `APIProvider` wraps the map tree. Raw `gmp-*` web components only for elements without React bindings (e.g. `gmp-polyline-3d`). -- **Icons**: Inline SVG or Lucide React. No icon font libraries. +- **Icons**: doo-iconik bridge (`src/lib/icons/`) using ``. Icon data sourced from [ajentik/doo-iconik](https://github.com/ajentik/doo-iconik) (MIT). No icon font libraries. ### Store Shape diff --git a/package-lock.json b/package-lock.json index 486f69f..09056f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "focus-trap-react": "^12.0.0", - "lucide-react": "^0.577.0", "next": "16.1.6", "next-themes": "^0.4.6", "react": "19.2.3", @@ -9054,15 +9053,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lucide-react": { - "version": "0.577.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", - "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/package.json b/package.json index 9929355..190083f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "focus-trap-react": "^12.0.0", - "lucide-react": "^0.577.0", "next": "16.1.6", "next-themes": "^0.4.6", "react": "19.2.3", diff --git a/src/components/TranscriptionConfirmation.tsx b/src/components/TranscriptionConfirmation.tsx index 7b898af..6df9e74 100644 --- a/src/components/TranscriptionConfirmation.tsx +++ b/src/components/TranscriptionConfirmation.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import { Check, RefreshCw } from "lucide-react"; +import { DooIcon } from "@/lib/icons"; import { cn } from "@/lib/utils"; import type { QualityResult } from "@/utils/transcriptionQuality"; @@ -89,7 +89,7 @@ export default function TranscriptionConfirmation({ )} aria-label="Confirm transcription" > - + Correct diff --git a/src/components/aac/AACResultCard.tsx b/src/components/aac/AACResultCard.tsx index 2529f1b..068497e 100644 --- a/src/components/aac/AACResultCard.tsx +++ b/src/components/aac/AACResultCard.tsx @@ -1,7 +1,7 @@ "use client"; import type { POI } from "@/types"; -import { MapPin, Footprints, Loader2, Phone } from "lucide-react"; +import { DooIcon } from "@/lib/icons"; import { useWalkingRoute } from "@/hooks/useWalkingRoute"; interface AACResultCardProps { @@ -28,7 +28,7 @@ export default function AACResultCard({ poi, distanceKm, onSelect }: AACResultCa >
- +

@@ -62,9 +62,9 @@ export default function AACResultCard({ poi, distanceKm, onSelect }: AACResultCa className="absolute top-2 right-2 flex items-center justify-center w-7 h-7 rounded-full bg-primary/10 hover:bg-primary/20 active:scale-90 transition-all disabled:opacity-60" > {isWalking ? ( -

-
} title="No centres found" diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index f5d6348..fbc2b1c 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useRef, useEffect, useCallback } from "react"; -import { ArrowUp } from "lucide-react"; +import { DooIcon } from "@/lib/icons"; import { cn } from "@/lib/utils"; import VoiceButton from "./VoiceButton"; @@ -126,9 +126,9 @@ export default function ChatInput({ onSend, isLoading }: ChatInputProps) { : "bg-muted/60 text-muted-foreground/40" )} > -
- +

@@ -389,7 +389,7 @@ export default function ChatPanel() { disabled={isActive} className="text-destructive border-destructive/30 hover:bg-destructive/10" > - + Retry

@@ -437,7 +437,7 @@ export default function ChatPanel() { )} > New messages - + )} diff --git a/src/components/chat/ToolResultCard.tsx b/src/components/chat/ToolResultCard.tsx index 11a6b6c..7234d84 100644 --- a/src/components/chat/ToolResultCard.tsx +++ b/src/components/chat/ToolResultCard.tsx @@ -1,7 +1,7 @@ "use client"; import { memo } from "react"; -import { MapPin, Calendar, Info, Star, Clock, Navigation } from "lucide-react"; +import { DooIcon } from "@/lib/icons"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; @@ -75,7 +75,7 @@ function LocationCard({ output }: { output: NavigateOutput }) { return ( - +

{output.message}

@@ -86,7 +86,7 @@ function LocationCard({ output }: { output: NavigateOutput }) {
- +
@@ -101,19 +101,19 @@ function LocationCard({ output }: { output: NavigateOutput }) {
{poi.address && ( - + {poi.address} )} {poi.hours && ( - + {poi.hours} )} {poi.rating && ( - + {poi.rating}/5 )} @@ -139,7 +139,7 @@ function EventListCard({ output }: { output: ShowEventsOutput }) { return ( - +

{message}

@@ -154,7 +154,7 @@ function EventListCard({ output }: { output: ShowEventsOutput }) {
- +

{message}

@@ -224,7 +224,7 @@ function CampusInfoCard({ output }: { output: CampusInfoOutput }) {
- +

{output.answer} @@ -238,7 +238,7 @@ function CampusInfoCard({ output }: { output: CampusInfoOutput }) { className="flex items-start gap-2 rounded-xl border border-border/50 p-2.5 text-xs transition-colors duration-150 active:bg-muted/30" > - +

{venue.name} {venue.address && ( @@ -250,13 +250,13 @@ function CampusInfoCard({ output }: { output: CampusInfoOutput }) {
{venue.hours && ( - + {venue.hours} )} {venue.rating && ( - + {venue.rating} )} diff --git a/src/components/chat/VoiceButton.tsx b/src/components/chat/VoiceButton.tsx index d1af84e..3753828 100644 --- a/src/components/chat/VoiceButton.tsx +++ b/src/components/chat/VoiceButton.tsx @@ -2,7 +2,7 @@ import { toast } from "sonner"; import { useSpeechRecognition } from "@/lib/voice/speech-recognition"; -import { Mic } from "lucide-react"; +import { DooIcon } from "@/lib/icons"; import { cn } from "@/lib/utils"; interface VoiceButtonProps { @@ -36,7 +36,7 @@ export default function VoiceButton({ onTranscript }: VoiceButtonProps) { title={isListening ? "Stop recording" : "Start voice input"} aria-label={isListening ? "Stop recording" : "Start voice input"} > - + ); } diff --git a/src/components/events/EventCard.tsx b/src/components/events/EventCard.tsx index 239e3e3..1cf8398 100644 --- a/src/components/events/EventCard.tsx +++ b/src/components/events/EventCard.tsx @@ -3,9 +3,14 @@ import { useRef, useState, useEffect } from "react"; import type { CampusEvent } from "@/types"; import { Badge } from "@/components/ui/badge"; -import { ExternalLink } from "lucide-react"; +import { DooIcon } from "@/lib/icons"; import { useAppStore } from "@/store/app-store"; -import { CATEGORY_ICON, DEFAULT_EVENT_ICON, CATEGORY_ICON_BG, DEFAULT_ICON_BG } from "@/lib/event-icons"; +import { + CATEGORY_ICON, + DEFAULT_EVENT_ICON, + CATEGORY_ICON_BG, + DEFAULT_ICON_BG, +} from "@/lib/event-icons"; interface EventCardProps { event: CampusEvent; @@ -40,13 +45,14 @@ export default function EventCard({ event, index = 0 }: EventCardProps) { const setStreetViewEvent = useAppStore((s) => s.setStreetViewEvent); const [isExpanded, setIsExpanded] = useState(false); const [contentHeight, setContentHeight] = useState(0); + const contentRef = useRef(null); useEffect(() => { + if (!isExpanded) return; if (contentRef.current) { setContentHeight(contentRef.current.scrollHeight); } }, [isExpanded]); - const contentRef = useRef(null); const handleClick = () => { setFlyToTarget({ lat: event.lat, lng: event.lng }); @@ -58,11 +64,9 @@ export default function EventCard({ event, index = 0 }: EventCardProps) { setStreetViewEvent(event); }; - const dateDisplay = event.endDate - ? `${event.date} – ${event.endDate}` - : event.date; + const dateDisplay = event.endDate ? `${event.date} – ${event.endDate}` : event.date; - const CategoryIcon = CATEGORY_ICON[event.category] ?? DEFAULT_EVENT_ICON; + const categoryIcon = CATEGORY_ICON[event.category] ?? DEFAULT_EVENT_ICON; const iconBg = CATEGORY_ICON_BG[event.category] ?? DEFAULT_ICON_BG; return ( @@ -73,10 +77,9 @@ export default function EventCard({ event, index = 0 }: EventCardProps) { style={{ "--card-index": index } as React.CSSProperties} className={`animate-card-slide-in w-full text-left rounded-2xl border border-border/60 border-l-4 ${CATEGORY_ACCENT[event.category] || "border-l-gray-300"} bg-card hover:bg-muted/30 active:bg-muted/50 active:scale-[0.985] transition-all duration-200 ease-out flex flex-col gap-3 shadow-[0_1px_3px_rgba(0,0,0,0.06)] hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] p-5`} > - {/* Header: icon + title + type indicator */}
-
@@ -88,7 +91,6 @@ export default function EventCard({ event, index = 0 }: EventCardProps) { )}
- {/* Date & time — prominent */}
Date @@ -104,7 +106,6 @@ export default function EventCard({ event, index = 0 }: EventCardProps) {
- {/* Location */}
Location @@ -120,7 +121,6 @@ export default function EventCard({ event, index = 0 }: EventCardProps) { )}
- {/* Tags row */}
{event.type && ( @@ -137,26 +137,20 @@ export default function EventCard({ event, index = 0 }: EventCardProps) { )}
- {/* Description */} {event.description && (

{event.description}

)} - {/* Expandable long description */} {event.longDescription && (
-
- {event.longDescription} -
+
{event.longDescription}
) : event.url ? ( @@ -219,7 +208,7 @@ export default function EventCard({ event, index = 0 }: EventCardProps) { onClick={(e) => e.stopPropagation()} className="flex items-center justify-center gap-2 bg-primary text-primary-foreground px-5 py-2.5 rounded-full text-sm font-semibold hover:opacity-90 active:opacity-80 active:scale-95 transition-all min-h-[44px] min-w-[44px] shadow-sm" > - + Join Online ) : null} diff --git a/src/components/events/EventFilter.tsx b/src/components/events/EventFilter.tsx index 45e1e9a..8f76af3 100644 --- a/src/components/events/EventFilter.tsx +++ b/src/components/events/EventFilter.tsx @@ -2,7 +2,7 @@ import type { DateRangePreset } from "@/types"; import { cn } from "@/lib/utils"; -import { X, SlidersHorizontal } from "lucide-react"; +import { DooIcon } from "@/lib/icons"; import { Select, SelectContent, @@ -47,7 +47,7 @@ export default function EventFilter({ {/* Section header with filter count */}
- + Filters {activeCount > 0 && ( @@ -66,7 +66,7 @@ export default function EventFilter({ }} className="animate-hero-fade-in flex items-center gap-1 text-xs text-primary hover:text-primary/80 font-medium transition-colors min-h-[44px] px-2" > - + Clear all )} diff --git a/src/components/events/EventsPanel.tsx b/src/components/events/EventsPanel.tsx index 5f439d2..c4f347f 100644 --- a/src/components/events/EventsPanel.tsx +++ b/src/components/events/EventsPanel.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { EmptyState } from "@/components/ui/empty-state"; -import { CalendarX, ChevronUp } from "lucide-react"; +import { DooIcon } from "@/lib/icons"; import { useCampusEvents } from "@/hooks/useCampusEvents"; import { useAppStore } from "@/store/app-store"; import type { DateRangePreset } from "@/types"; @@ -43,7 +43,6 @@ export default function EventsPanel() { setMapEventMarkers(events); }, [events, setMapEventMarkers]); - // Track scroll to show/hide scroll-to-top button useEffect(() => { const viewport = scrollRef.current?.querySelector("[data-radix-scroll-area-viewport]"); if (!viewport) return; @@ -52,7 +51,7 @@ export default function EventsPanel() { }; viewport.addEventListener("scroll", handleScroll, { passive: true }); return () => viewport.removeEventListener("scroll", handleScroll); - }, [isLoading]); + }, []); const scrollToTop = useCallback(() => { const viewport = scrollRef.current?.querySelector("[data-radix-scroll-area-viewport]"); @@ -88,7 +87,7 @@ export default function EventsPanel() { icon={
- +
} @@ -119,7 +118,6 @@ export default function EventsPanel() { )} - {/* Scroll to top FAB */} {showScrollTop && ( )}
diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 51031fe..043b0e7 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -3,19 +3,7 @@ import { useState, useCallback, useRef, useEffect } from "react"; import Image from "next/image"; import dynamic from "next/dynamic"; -import { - Volume2, - VolumeX, - Minus, - Maximize2, - GripVertical, - MessageSquare, - Calendar, - Building2, - // SquarePen — hidden for senior UX - // Sun, Moon — theme toggle hidden for senior UX -} from "lucide-react"; -// import { useTheme } from "next-themes"; — theme toggle hidden for senior UX +import { DooIcon } from "@/lib/icons"; import { APIProvider } from "@vis.gl/react-google-maps"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useAppStore, type PanelId } from "@/store/app-store"; @@ -67,37 +55,9 @@ function BrandHeader({ priority />
- - AskSUSSi - + AskSUSSi
- {/* Feedback / New-chat button hidden for senior UX - {onNewChat && ( - - )} - */} - {/* Theme toggle removed — forcing light mode (handled externally) - {mounted && ( - - )} - */} {extraButtons}
@@ -140,8 +100,7 @@ export default function AppShell() { (e: React.MouseEvent | React.TouchEvent) => { e.preventDefault(); isResizing.current = true; - startX.current = - "touches" in e ? e.touches[0].clientX : e.clientX; + startX.current = "touches" in e ? e.touches[0].clientX : e.clientX; startWidth.current = panelWidth; document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; @@ -155,15 +114,18 @@ export default function AppShell() { } }, []); - const handleSnapChange = useCallback((snap: SnapName) => { - const mapping: Record = { - mini: "collapsed", - peek: "peek", - half: "expanded", - full: "expanded", - }; - setMobileSheet(mapping[snap]); - }, [setMobileSheet]); + const handleSnapChange = useCallback( + (snap: SnapName) => { + const mapping: Record = { + mini: "collapsed", + peek: "peek", + half: "expanded", + full: "expanded", + }; + setMobileSheet(mapping[snap]); + }, + [setMobileSheet], + ); const mobileSnapToRef = useRef<((snap: SnapName) => void) | null>(null); @@ -176,12 +138,8 @@ export default function AppShell() { mobileSnapToRef.current?.(mapping[mobileSheet]); }, [mobileSheet]); - const toggleTts = useCallback( - () => setTtsEnabled(!ttsEnabled), - [ttsEnabled, setTtsEnabled], - ); + const toggleTts = useCallback(() => setTtsEnabled(!ttsEnabled), [ttsEnabled, setTtsEnabled]); - // Close detail and return to default sheet content const handleCloseDetail = useCallback(() => { setSheetContentMode("default"); setSelectedPOI(null); @@ -189,7 +147,6 @@ export default function AppShell() { setMobileSheet("expanded"); }, [setSheetContentMode, setSelectedPOI, setSelectedEvent, setMobileSheet]); - // Handle navigate from detail cards const handleNavigatePOI = useCallback(() => { if (selectedPOI) { setSelectedDestination(selectedPOI); @@ -210,13 +167,9 @@ export default function AppShell() { useEffect(() => { const handleMove = (e: MouseEvent | TouchEvent) => { if (!isResizing.current) return; - const clientX = - "touches" in e ? e.touches[0].clientX : e.clientX; + const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; const delta = clientX - startX.current; - const newWidth = Math.min( - MAX_WIDTH, - Math.max(MIN_WIDTH, startWidth.current + delta), - ); + const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth.current + delta)); setPanelWidth(newWidth); }; @@ -245,15 +198,11 @@ export default function AppShell() { peek: sheetContentMode !== "default" ? "280px" : "140px", expanded: "75dvh", }; - document.documentElement.style.setProperty( - "--sheet-height", - heightMap[mobileSheet] ?? "64px" - ); + document.documentElement.style.setProperty("--sheet-height", heightMap[mobileSheet] ?? "64px"); }, [mobileSheet, sheetContentMode]); return (
- {/* Desktop sidebar */} - {/* Desktop resize handle */} {!minimized && ( )} - {/* Desktop minimize restore button */} {minimized && ( )} @@ -351,7 +292,7 @@ export default function AppShell() {
-
AskSUSSi @@ -385,41 +326,26 @@ export default function AppShell() { className="flex-1 flex flex-col min-h-0" > - - - + - + @@ -427,10 +353,7 @@ export default function AppShell() { )} - +
diff --git a/src/components/layout/HeroIntro.tsx b/src/components/layout/HeroIntro.tsx index 5b8d09d..82a52ee 100644 --- a/src/components/layout/HeroIntro.tsx +++ b/src/components/layout/HeroIntro.tsx @@ -2,17 +2,17 @@ import { useEffect, useRef, useState, useCallback } from "react"; import Image from "next/image"; -import { ArrowRight, Navigation, Globe, MessageCircle, CalendarDays } from "lucide-react"; +import { DooIcon, type IconName } from "@/lib/icons"; import { lookupAerialVideo } from "@/lib/maps/aerial-view"; const SUSS_ADDRESS = "Singapore University of Social Sciences, 463 Clementi Road, Singapore 599494"; -const FEATURES = [ - { icon: Navigation, label: "3D Campus Map", desc: "Explore SUSS in photorealistic 3D" }, - { icon: Globe, label: "Street View", desc: "Walk through campus & indoor spaces" }, - { icon: MessageCircle, label: "AI Chat & Voice", desc: "Ask SUSSi anything about campus" }, - { icon: CalendarDays, label: "Events & Navigation", desc: "Discover events, navigate to venues" }, +const FEATURES: ReadonlyArray<{ icon: IconName; label: string; desc: string }> = [ + { icon: "navigation", label: "3D Campus Map", desc: "Explore SUSS in photorealistic 3D" }, + { icon: "globe", label: "Street View", desc: "Walk through campus & indoor spaces" }, + { icon: "message", label: "AI Chat & Voice", desc: "Ask SUSSi anything about campus" }, + { icon: "calendar", label: "Events & Navigation", desc: "Discover events, navigate to venues" }, ]; interface HeroIntroProps { @@ -62,9 +62,7 @@ export default function HeroIntro({ onEnter }: HeroIntroProps) { if (!video) return; const updateProgress = () => { if (video.buffered.length > 0 && video.duration > 0) { - const pct = - (video.buffered.end(video.buffered.length - 1) / video.duration) * - 100; + const pct = (video.buffered.end(video.buffered.length - 1) / video.duration) * 100; setProgress(pct); } }; @@ -74,7 +72,7 @@ export default function HeroIntro({ onEnter }: HeroIntroProps) { video.removeEventListener("progress", updateProgress); video.removeEventListener("loadeddata", updateProgress); }; - }, [videoUrl]); + }, []); const handleEnter = useCallback(() => { setFadeOut(true); @@ -87,18 +85,15 @@ export default function HeroIntro({ onEnter }: HeroIntroProps) { fadeOut ? "opacity-0 pointer-events-none" : "opacity-100" }`} > - {/* Animated gradient fallback */}
- {/* Radial glow accents */}
- {/* Loading progress */} {!videoFailed && !videoReady && (
@@ -110,7 +105,6 @@ export default function HeroIntro({ onEnter }: HeroIntroProps) {
)} - {/* Aerial video background */} {videoUrl && (