routes.tsx
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { BasePage } from './pages/BasePage';
import { Films } from './pages/Films';
import { Home } from './pages/Home';
import { FilmDetails } from './pages/FilmDetails';
export function AppRoutes() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<BasePage />}>
<Route index element={<Home />} />
<Route path="/filmes" element={<Films />} />
<Route path="/filme/:id" element={<FilmDetails />} />
</Route>
</Routes>
</BrowserRouter>
);
}
Header
// src/components/Header.tsx
import { Link } from 'react-router-dom';
import { useScrolled } from '../../../hooks/useScrolled';
export function Header() {
const scrolled = useScrolled(24);
return (
<header
className={[
'fixed inset-x-0 top-0 z-50 h-20 px-10',
'flex items-center',
'transition-colors duration-300',
'border-b',
scrolled
? 'bg-surface border-white/10 backdrop-blur-md'
: 'bg-transparent border-transparent',
].join(' ')}
>
<Link to="/" className="font-roboto font-semibold text-2xl">
DevMovie
</Link>
</header>
);
}
Footer
export function Footer() {
return (
<footer className="h-14 mt-16 bg-footer-bg text-footer-color flex justify-center items-center">
<p className="font-light">
Desenvolvido por{' '}
<a
href="<https://www.arturbomtempo.dev/>"
target="_blank"
className="hover:underline underline-offset-4 decoration-1 transition"
>
Artur Bomtempo
</a>
</p>
</footer>
);
}
Container
import type { ReactNode } from 'react';
interface ContainerProps {
children: ReactNode;
}
export function Container({ children }: ContainerProps) {
return <section>{children}</section>;
}
hooks/useScrolled.ts
// src/hooks/useScrolled.ts
import { useEffect, useState } from 'react';
export function useScrolled(threshold = 16) {
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
let ticking = false;
const onScroll = () => {
const y = window.scrollY || document.documentElement.scrollTop;
if (!ticking) {
window.requestAnimationFrame(() => {
setScrolled(y > threshold);
ticking = false;
});
ticking = true;
}
};
onScroll(); // define o estado correto já no mount
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, [threshold]);
return scrolled;
}
pages/BasePage/index.tsx
import { Outlet } from 'react-router-dom';
import { Container } from '../../components/Common/Container';
import { Header } from '../../components/Common/Header';
import { Footer } from '../../components/Common/Footer';
export function BasePage() {
return (
<main>
<Header />
<Container>
<Outlet />
</Container>
<Footer />
</main>
);
}
Home/Title/index.tsx
interface TitleProps {
text: string;
}
export function Title({ text }: TitleProps) {
return <h2 className="font-medium font-libre text-2xl text-center my-8 md:text-4xl">{text}</h2>;
}