Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion public/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 19 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,11 +22,21 @@ function App() {
<Route element={<Layout />}>
<Route path="/" element={<Landing />} />
<Route path="/about" element={<About />} />
<Route path="/build" element={<Build />} />

{/* Build routes */}
<Route path="/build" element={<BuildLanding />} />
<Route path="/build/docs" element={<BuildDocs />} />

{/* Learn routes */}
<Route path="/learn" element={<LearnLanding />} />
<Route path="/learn/coursekit" element={<LearnCourseKit />} />

{/* Race routes */}
<Route path="/race" element={<RaceLanding />} />
<Route path="/race/events" element={<RaceEvents />} />

<Route path="/course" element={<Course />} />
<Route path="/learn" element={<Learn />} />
<Route path="/news" element={<News />} />
<Route path="/race" element={<RaceCalendar />} />
<Route path="/research" element={<Research />} />
<Route path="/rules" element={<Rules />} />
</Route>
Expand Down
6 changes: 3 additions & 3 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -67,7 +67,7 @@ export default function Footer() {
<div>
<h3 className="font-semibold text-white mb-4">Community</h3>
<a
href="https://join.slack.com/t/robo-racer/shared_invite/zt-2pq4fuyjq-gTUflzeZDKDDGjuVoeZqNg"
href="https://join.slack.com/t/robo-racer/shared_invite/zt-3r2d2fe4k-6pvIKjwJH_M28DTyEuR5uQ"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-white/10 hover:bg-white/20 rounded-md transition-colors duration-200 text-sm font-medium mb-4"
Expand Down
8 changes: 4 additions & 4 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import { useLocation } from "react-router-dom";
export default function Layout() {
const location = useLocation();
const currentPath = location.pathname;
const isAltLayout = currentPath === "/learn" || currentPath === "/build" || currentPath === "/course";
const isAltLayout = currentPath === "/learn/coursekit" || currentPath === "/build/docs" || currentPath === "/course";
return (
<div className={`flex flex-col h-[100svh] ${isAltLayout && "overflow-hidden"}`}>
<div className={`flex flex-col ${isAltLayout ? "h-[100svh] overflow-hidden" : "min-h-screen"}`}>
<Navbar />
<main>
<main className="flex-grow">
<Outlet />
</main>
<Footer />
{!isAltLayout && <Footer />}
</div>
);
}
106 changes: 78 additions & 28 deletions src/components/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -44,7 +59,8 @@ export default function Navbar() {

{/* Desktop Links */}
<div className="nav-links">
{links.map((link) => (
{/* Visible Links */}
{visibleLinks.map((link) => (
<Link
key={link.href}
to={link.href}
Expand All @@ -54,6 +70,26 @@ export default function Navbar() {
</Link>
))}

{/* Hamburger Menu Button (Desktop) */}
<button
onClick={() => setMenuOpen(!menuOpen)}
className="nav-link inline-flex items-center gap-1 cursor-pointer"
aria-label="More menu items"
>
<span>More</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{menuOpen ? (
<path d="M18 6L6 18M6 6l12 12"/>
) : (
<>
<circle cx="12" cy="12" r="1"/>
<circle cx="12" cy="5" r="1"/>
<circle cx="12" cy="19" r="1"/>
</>
)}
</svg>
</button>

{/* CTA Buttons */}
<a
href="https://autodrive-ecosystem.github.io/"
Expand All @@ -70,7 +106,7 @@ export default function Navbar() {
</a>

<a
href="https://join.slack.com/t/robo-racer/shared_invite/zt-2pq4fuyjq-gTUflzeZDKDDGjuVoeZqNg"
href="https://join.slack.com/t/robo-racer/shared_invite/zt-3r2d2fe4k-6pvIKjwJH_M28DTyEuR5uQ"
target="_blank"
rel="noopener noreferrer"
className="nav-cta"
Expand All @@ -81,7 +117,7 @@ export default function Navbar() {

{/* Mobile Menu Button */}
<button
className="md:hidden"
className="md:hidden flex items-center gap-1"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
>
Expand All @@ -94,46 +130,60 @@ export default function Navbar() {
</svg>
</button>

{/* Mobile Menu */}
{/* Dropdown Menu (Desktop & Mobile) */}
<AnimatePresence>
{menuOpen && (
<motion.div
className="mobile-menu md:hidden"
className="dropdown-menu"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
{links.map((link) => (
{/* Show all links on mobile, only hamburger links on desktop */}
{hamburgerLinks.map((link) => (
<Link
key={link.href}
to={link.href}
className="mobile-menu-link"
className="dropdown-menu-link"
>
{link.text}
</Link>
))}
<a
href="https://autodrive-ecosystem.github.io/"
target="_blank"
rel="noopener noreferrer"
className="mobile-menu-link flex items-center gap-2"
>
Simulator
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
</a>
<a
href="https://join.slack.com/t/robo-racer/shared_invite/zt-2pq4fuyjq-gTUflzeZDKDDGjuVoeZqNg"
target="_blank"
rel="noopener noreferrer"
className="mobile-menu-link"
>
Join Community
</a>

{/* Mobile-only: Show visible links + external links */}
<div className="md:hidden border-t border-gray-200 pt-2 mt-2">
{visibleLinks.map((link) => (
<Link
key={`mobile-${link.href}`}
to={link.href}
className="dropdown-menu-link"
>
{link.text}
</Link>
))}
<a
href="https://autodrive-ecosystem.github.io/"
target="_blank"
rel="noopener noreferrer"
className="dropdown-menu-link flex items-center gap-2"
>
Simulator
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
</a>
<a
href="https://join.slack.com/t/robo-racer/shared_invite/zt-3r2d2fe4k-6pvIKjwJH_M28DTyEuR5uQ"
target="_blank"
rel="noopener noreferrer"
className="dropdown-menu-link"
>
Join Community
</a>
</div>
</motion.div>
)}
</AnimatePresence>
Expand Down
131 changes: 131 additions & 0 deletions src/components/VideoSlider.tsx
Original file line number Diff line number Diff line change
@@ -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<number[]>([]);

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 (
<div className="relative w-full max-w-7xl mx-auto px-6">
{/* Carousel */}
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex gap-4">
{videos.map((video, index) => (
<div
key={index}
className="flex-[0_0_100%] min-w-0 md:flex-[0_0_calc(50%-8px)] lg:flex-[0_0_calc(33.333%-11px)]"
>
<div className="relative pb-[56.25%] bg-gray-900 rounded-lg overflow-hidden shadow-lg">
<iframe
className="absolute top-0 left-0 w-full h-full"
src={getEmbedUrl(video)}
title={`Video ${index + 1}`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
</div>
))}
</div>
</div>

{/* Navigation Buttons */}
<button
className="absolute left-2 top-1/2 -translate-y-1/2 bg-white/90 hover:bg-white text-gray-900 rounded-full p-3 shadow-lg hover:shadow-xl transition-all z-10 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={scrollPrev}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
<button
className="absolute right-2 top-1/2 -translate-y-1/2 bg-white/90 hover:bg-white text-gray-900 rounded-full p-3 shadow-lg hover:shadow-xl transition-all z-10 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={scrollNext}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>

{/* Dots Indicator */}
<div className="flex justify-center gap-2 mt-6">
{scrollSnaps.map((_, index) => (
<button
key={index}
className={`w-2 h-2 rounded-full transition-all ${
index === selectedIndex
? 'bg-blue-900 w-8'
: 'bg-gray-300 hover:bg-gray-400'
}`}
onClick={() => scrollTo(index)}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
</div>
);
}
Loading