diff --git a/public/rules.md b/public/rules.md index a9ed479..cb424c7 100644 --- a/public/rules.md +++ b/public/rules.md @@ -32,7 +32,7 @@ The competition is organized as an in-person competition. Teams can register for the competition using a [registration form](https://forms.gle/FdfY9sKXREdu772u6). -The preferred communication method with the organizers is the _#ICRA2025_ channel on [Roboracer-teams Slack](https://join.slack.com/t/robo-racer/shared_invite/zt-2pq4fuyjq-gTUflzeZDKDDGjuVoeZqNg). +The preferred communication method with the organizers is the _#ICRA2025_ channel on [Roboracer-teams Slack](https://join.slack.com/t/robo-racer/shared_invite/zt-3r2d2fe4k-6pvIKjwJH_M28DTyEuR5uQ). # 2. In-person (physical) competition diff --git a/src/App.tsx b/src/App.tsx index 7841907..516017e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,11 +2,14 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import Layout from "./components/Layout"; import Landing from "./pages/Landing"; import About from "./pages/About"; -import Build from "./pages/Build"; +import BuildLanding from "./pages/BuildLanding"; +import BuildDocs from "./pages/BuildDocs"; import Course from "./pages/Course"; -import Learn from "./pages/Learn"; +import LearnLanding from "./pages/LearnLanding"; +import LearnCourseKit from "./pages/LearnCourseKit"; import News from "./pages/News"; -import RaceCalendar from "./pages/Race"; +import RaceLanding from "./pages/RaceLanding"; +import RaceEvents from "./pages/RaceEvents"; import Research from "./pages/Research"; import Rules from "./pages/Rules"; // import Events from "./pages/Events"; @@ -19,11 +22,21 @@ function App() { }> } /> } /> - } /> + + {/* Build routes */} + } /> + } /> + + {/* Learn routes */} + } /> + } /> + + {/* Race routes */} + } /> + } /> + } /> - } /> } /> - } /> } /> } /> diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index e0d8022..938df26 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -3,8 +3,8 @@ import { useLocation } from "react-router-dom"; export default function Footer() { const location = useLocation(); const isAltFooter = - location.pathname === "/learn" || - location.pathname === "/build" || + location.pathname === "/learn/coursekit" || + location.pathname === "/build/docs" || location.pathname === "/course"; if (isAltFooter) return null; @@ -67,7 +67,7 @@ export default function Footer() {

Community

+
-
+
-
+ {!isAltLayout &&
}
); } diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index 14fad0c..3dd3300 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -2,6 +2,13 @@ import { useState, useEffect } from "react"; import { Link, useLocation } from "react-router-dom"; import { motion, AnimatePresence } from "framer-motion"; +// ============================================ +// CONFIGURATION +// ============================================ + +// Items displayed outside hamburger menu (on desktop) +const VISIBLE_LINKS = ["build", "learn", "race"]; + const links = [ { href: "/about", text: "About" }, { href: "/build", text: "Build" }, @@ -17,6 +24,14 @@ export default function Navbar() { const [menuOpen, setMenuOpen] = useState(false); const [scrolled, setScrolled] = useState(false); + // Split links based on visibility configuration + const visibleLinks = links.filter(link => + VISIBLE_LINKS.includes(link.href.replace('/', '').toLowerCase()) + ); + const hamburgerLinks = links.filter(link => + !VISIBLE_LINKS.includes(link.href.replace('/', '').toLowerCase()) + ); + // Detect scroll for shadow enhancement useEffect(() => { const handleScroll = () => { @@ -44,7 +59,8 @@ export default function Navbar() { {/* Desktop Links */}
- {links.map((link) => ( + {/* Visible Links */} + {visibleLinks.map((link) => ( ))} + {/* Hamburger Menu Button (Desktop) */} + + {/* CTA Buttons */} setMenuOpen(!menuOpen)} aria-label="Toggle menu" > @@ -94,46 +130,60 @@ export default function Navbar() { - {/* Mobile Menu */} + {/* Dropdown Menu (Desktop & Mobile) */} {menuOpen && ( - {links.map((link) => ( + {/* Show all links on mobile, only hamburger links on desktop */} + {hamburgerLinks.map((link) => ( {link.text} ))} - - Simulator - - - - - - - - Join Community - + + {/* Mobile-only: Show visible links + external links */} +
+ {visibleLinks.map((link) => ( + + {link.text} + + ))} + + Simulator + + + + + + + + Join Community + +
)} diff --git a/src/components/VideoSlider.tsx b/src/components/VideoSlider.tsx new file mode 100644 index 0000000..ea019d4 --- /dev/null +++ b/src/components/VideoSlider.tsx @@ -0,0 +1,131 @@ +import { useCallback, useEffect, useState } from 'react'; +import useEmblaCarousel from 'embla-carousel-react'; + +interface VideoSliderProps { + videos: string[]; +} + +export default function VideoSlider({ videos }: VideoSliderProps) { + const [emblaRef, emblaApi] = useEmblaCarousel({ + loop: true, + align: 'center', + }); + const [selectedIndex, setSelectedIndex] = useState(0); + const [scrollSnaps, setScrollSnaps] = useState([]); + + const scrollPrev = useCallback(() => { + if (emblaApi) emblaApi.scrollPrev(); + }, [emblaApi]); + + const scrollNext = useCallback(() => { + if (emblaApi) emblaApi.scrollNext(); + }, [emblaApi]); + + const scrollTo = useCallback( + (index: number) => { + if (emblaApi) emblaApi.scrollTo(index); + }, + [emblaApi] + ); + + const onSelect = useCallback(() => { + if (!emblaApi) return; + setSelectedIndex(emblaApi.selectedScrollSnap()); + }, [emblaApi]); + + useEffect(() => { + if (!emblaApi) return; + onSelect(); + setScrollSnaps(emblaApi.scrollSnapList()); + emblaApi.on('select', onSelect); + return () => { + emblaApi.off('select', onSelect); + }; + }, [emblaApi, onSelect]); + + // Convert YouTube URLs to embed format + const getEmbedUrl = (url: string) => { + const videoId = url.match(/(?:youtu\.be\/|youtube\.com(?:\/embed\/|\/v\/|\/watch\?v=|\/watch\?.+&v=))([^&\n?#]+)/)?.[1]; + return `https://www.youtube.com/embed/${videoId}`; + }; + + return ( +
+ {/* Carousel */} +
+
+ {videos.map((video, index) => ( +
+
+ */} +

Upcoming Events

+
+ {upcomingEvents.map((event, index) => { + const isTechfest = + (event.title && event.title.toLowerCase().includes("techfest")) || + (event.url && event.url.includes("techfest")); + + return ( +
{ + if (isTechfest) { + setInlineEventTitle(event.title ?? null); + setShowEventsInline((s) => !s); + } else { + setShowEventsInline(false); + } + }} + > +

{event.title}

+ {event.dates &&

{event.dates}

} + {event.location &&

{event.location}

} + {event.url && ( + e.stopPropagation()} + > + View details + + )} +
+ ); + })} +
+ + {/* Inline events content (e.g. Events page) */} + {showEventsInline && ( +
+ +
+ )} + +

Past Events

+
+ {pastRaces.map((race, index) => ( + +

{race.name}

+
+ ))} +
+
+ ); + } diff --git a/src/pages/RaceLanding.tsx b/src/pages/RaceLanding.tsx new file mode 100644 index 0000000..6e02ff9 --- /dev/null +++ b/src/pages/RaceLanding.tsx @@ -0,0 +1,192 @@ +import { Link } from "react-router-dom"; + +export default function RaceLanding() { + return ( +
+ {/* Hero Section */} +
+
+
+
+ 🏁 Compete & Race +
+

+ Compete in Autonomous Racing +

+

+ Join international competitions, test your algorithms against the best, and push the boundaries of autonomous racing technology. +

+
+ + View Upcoming Events + + + + + + Competition Rules + +
+
+
+ + {/* Competition Types Section */} +
+
+

+ Competition Formats +

+
+
+
+

Time Trials

+

+ Race against the clock to achieve the fastest lap times on challenging tracks with tight corners and obstacles. +

+
+
+
🏎️
+

Head-to-Head Racing

+

+ Compete directly against other autonomous vehicles, requiring strategic overtaking and defensive driving. +

+
+
+
🌐
+

Multi-Agent Scenarios

+

+ Coordinate with teammates or compete in complex scenarios with multiple vehicles on track. +

+
+
+
+
+ + {/* Event Highlights Section */} +
+
+

+ Racing Opportunities +

+
+
+

📅 Upcoming Events

+

+ View the schedule of upcoming competitions, workshops, and racing events worldwide. +

+ + See Schedule → + +
+
+

📋 Competition Rules

+

+ Learn about race formats, technical requirements, and competition guidelines. +

+ + Read Rules → + +
+
+

🏆 Past Results

+

+ Browse results and highlights from previous competitions around the world. +

+ + View Results → + +
+
+

🎯 Practice

+

+ Test your algorithms in simulation before competing in real-world events. +

+ + Launch Simulator → + +
+
+
+
+ + {/* Stats Section */} +
+
+

+ Global Racing Community +

+
+
+
24+
+
International Events
+
+
+
20+
+
Countries
+
+
+
100+
+
Racing Teams
+
+
+
+
+ + {/* Locations Section */} + {/*
+
+

+ Race Around the World +

+

+ We've hosted competitions in major cities including New York, Pittsburgh, Portugal, South Korea, Italy, Canada, and more. +

+
+ {['🇺🇸 USA', '🇵🇹 Portugal', '🇰🇷 South Korea', '🇮🇹 Italy', '🇨🇦 Canada', '🇬🇧 UK', '🇩🇪 Germany', '🇯🇵 Japan'].map((location) => ( + + {location} + + ))} +
+
+
*/} + + {/* CTA Section */} +
+
+

+ Ready to Race? +

+

+ Join our next competition and test your autonomous racing skills against the world's best. +

+
+ + View Upcoming Events + + + + + + Join Community + +
+
+
+
+ ); +}