Загрузка данных
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MovieBox</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
{
"name": "moviebox-router-template",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.1",
"vite": "^5.4.0"
}
}
Максым:
Понял, нужен код текстом, чтобы самому внести изменения. Ниже показываю, в каком файле что заменить в твоём исходном проекте.
1. Установи React Router
В терминале Codespaces, в папке проекта, выполни:
npm install react-router-dom@6.30.3
Файлы package.json и package-lock.json обновятся автоматически.
**2. Файл src/main.jsx — замени весь код**
Здесь приложение оборачивается в BrowserRouter, чтобы работала маршрутизация.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App.jsx';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
**3. Создай файл src/components/Layout.jsx**
В папке components создай новый файл Layout.jsx и вставь:
import { Outlet } from 'react-router-dom';
import Header from './Header.jsx';
export default function Layout() {
return (
<>
<Header />
<main className="content">
<Outlet />
</main>
</>
);
}
`Header — общая шапка. На месте Outlet появляется страница, соответствующая адресу.
**4. Файл src/App.jsx — замени весь код**
Здесь перечислены все маршруты приложения.
``jsx
import { Routes, Route } from 'react-router-dom';
import Layout from './components/Layout.jsx';
import HomePage from './pages/HomePage.jsx';
import MoviesPage from './pages/MoviesPage.jsx';
import MoviePage from './pages/MoviePage.jsx';
import SearchPage from './pages/SearchPage.jsx';
import AboutPage from './pages/AboutPage.jsx';
import ContactsPage from './pages/ContactsPage.jsx';
import NotFoundPage from './pages/NotFoundPage.jsx';
export default function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<HomePage />} />
<Route path="movies" element={<MoviesPage />} />
<Route path="movies/:id" element={<MoviePage />} />
<Route path="search" element={<SearchPage />} />
<Route path="about" element={<AboutPage />} />
<Route path="contacts" element={<ContactsPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
);
}
Например, `/movies/inception` откроет страницу фильма, а `inception` будет значением параметра `id`.
**5. Файл `src/components/Header.jsx` — замени весь код**
Здесь будут ссылки между страницами, подсветка активного пункта и поиск. Значение поиска хранится в URL, без `useState`.
jsx
import {
Link,
NavLink,
useNavigate,
useSearchParams,
} from 'react-router-dom';
export default function Header() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') || '';
function handleChange(event) {
const nextParams = new URLSearchParams(searchParams);
const value = event.target.value;
if (value) {
nextParams.set('q', value);
} else {
nextParams.delete('q');
}
setSearchParams(nextParams, { replace: true });
}
function handleSubmit(event) {
event.preventDefault();
const value = query.trim();
navigate(
value
? '/search?q=' + encodeURIComponent(value)
: '/search'
);
}
function navClassName({ isActive }) {
return 'nav-item' + (isActive ? ' active' : '');
}
return (
<header className="header">
<div className="header-inner">
<Link to="/" className="logo">
<span className="logo-icon">▶</span>
<span>MovieBox</span>
</Link>
<nav className="nav" aria-label="Основная навигация">
<NavLink to="/" end className={navClassName}>
Главная
</NavLink>
<NavLink to="/movies" className={navClassName}>
Фильмы
</NavLink>
<NavLink to="/about" className={navClassName}>
О проекте
</NavLink>
<NavLink to="/contacts" className={navClassName}>
Контакты
</NavLink>
</nav>
<form
className="search"
role="search"
onSubmit={handleSubmit}
>
<span className="search-icon" aria-hidden="true">
⌕
</span>
<input
type="search"
value={query}
onChange={handleChange}
placeholder="Поиск фильмов"
aria-label="Поиск фильмов"
/>
<button
className="search-submit"
type="submit"
aria-label="Найти"
>
→
</button>
</form>
</div>
</header>
);
}
При вводе текст записывается в параметр `q`. При нажатии **Enter** или кнопки **→** открывается страница результатов.
**6. Файл `src/components/GenreFilter.jsx` — замени весь код**
Кнопки будут изменять параметр `genre` в адресе.
jsx
import { useSearchParams } from 'react-router-dom';
export default function GenreFilter({ genres }) {
const [searchParams, setSearchParams] = useSearchParams();
const selectedGenre = searchParams.get('genre') || '';
function handleSelect(genreId) {
const nextParams = new URLSearchParams(searchParams);
if (genreId) {
nextParams.set('genre', genreId);
} else {
nextParams.delete('genre');
}
setSearchParams(nextParams);
}
return (
<div className="genres">
{genres.map((genre) => (
<button
key={genre.id || 'all'}
type="button"
onClick={() => handleSelect(genre.id)}
className={
'genre-btn' +
(genre.id === selectedGenre ? ' active' : '')
}
aria-pressed={genre.id === selectedGenre}
>
{genre.label}
</button>
))}
</div>
);
}
При выборе «Драмы» адрес станет `/movies?genre=drama`. Кнопка «Все» удаляет параметр жанра.
**7. Файл `src/components/MovieCard.jsx` — замени весь код**
Обычная ссылка заменяется на `Link`, чтобы карточка открывалась без перезагрузки приложения.
jsx
import { Link } from 'react-router-dom';
export default function MovieCard({ movie }) {
return (
<Link to={/movies/${movie.id}} className="movie-card">
<div
className="poster"
style={{ background: movie.color }}
>
<span className="poster-mark">
{movie.title[0]}
</span>
<span className="poster-year">
{movie.year}
</span>
</div>
<div className="movie-info">
<h3>{movie.title}</h3>
<p>
{movie.genreLabel} · {movie.year}
</p>
<div className="rating">
<span>★</span> {movie.rating}
</div>
</div>
</Link>
);
}
**8. Файл `src/pages/HomePage.jsx` — внеси три изменения**
В начало файла добавь импорт:
jsx
import { Link } from 'react-router-dom';
Найди:
jsx
<a href="/movies" className="primary-btn">
Открыть каталог
</a>
Замени на:
jsx
<Link to="/movies" className="primary-btn">
Открыть каталог
</Link>
Также найди:
jsx
<a href="/movies" className="text-link">
Все фильмы →
</a>
Замени на:
jsx
<Link to="/movies" className="text-link">
Все фильмы →
</Link>
Остальной код этого файла оставь.
**9. Файл `src/pages/MoviesPage.jsx` — замени весь код**
Страница читает выбранный жанр из URL и фильтрует фильмы.
jsx
import { useSearchParams } from 'react-router-dom';
import MovieCard from '../components/MovieCard.jsx';
import GenreFilter from '../components/GenreFilter.jsx';
import { movies, genres } from '../data/movies.js';
export default function MoviesPage() {
const [searchParams] = useSearchParams();
const selectedGenre = searchParams.get('genre') || '';
const visibleMovies = selectedGenre
? movies.filter((movie) => movie.genre === selectedGenre)
: movies;
return (
<section className="page-shell">
<span className="eyebrow">CATALOG</span>
<h1 className="page-title">Все фильмы</h1>
<p className="page-description">
Выберите жанр, чтобы найти фильм для просмотра.
</p>
<GenreFilter genres={genres} />
<div className="movie-grid">
{visibleMovies.length === 0 && (
<div className="empty-search">
Фильмов этого жанра нет.
</div>
)}
{visibleMovies.map((movie) => (
<MovieCard key={movie.id} movie={movie} />
))}
</div>
</section>
);
}
**10. Файл `src/pages/MoviePage.jsx` — внеси четыре изменения**
В начало файла добавь:
jsx
import {
Link,
useParams,
useNavigate,
} from 'react-router-dom';
Найди:
jsx
const id = 'inception';
const movie = getMovieById(id);
Замени на:
jsx
const { id } = useParams();
const navigate = useNavigate();
const movie = getMovieById(id);
Теперь фильм определяется по адресу, а не всегда равен `inception`.
Далее найди:
jsx
if (!movie) {
return <div className="inline-error">Фильм не найден.</div>;
}
Замени на:
jsx
if (!movie) {
return (
<section className="page-shell">
<h1 className="page-title">
Фильм не найден
</h1>
<p className="page-description">
В каталоге нет фильма с ID «{id}».
</p>
<Link to="/movies" className="primary-btn">
Открыть каталог
</Link>
</section>
);
}
Внизу найди кнопку:
jsx
<button className="secondary-btn" type="button">
← Назад
</button>
Замени на:
jsx
<button
className="secondary-btn"
type="button"
onClick={() => navigate(-1)}
>
← Назад
</button>
Остальной код страницы оставь.
**11. Файл `src/pages/SearchPage.jsx` — внеси два изменения**
В начало файла добавь:
jsx
import { useSearchParams } from 'react-router-dom';
Найди внутри функции `SearchPage`:
jsx
const query = '';
Замени на:
jsx
const [searchParams] = useSearchParams();
const query = (searchParams.get('q') || '').trim();
**Остальной код уже написан в исходном файле:** он ищет фильм по русскому и оригинальному названию и показывает результаты.
**12. Файл `src/pages/NotFoundPage.jsx` — замени весь код**
jsx
import {
useLocation,
useNavigate,
} from 'react-router-dom';
import StatusBlock from '../components/StatusBlock.jsx';
export default function NotFoundPage() {
const location = useLocation();
const navigate = useNavigate();
return (
<section className="page-shell">
<StatusBlock
title="Страница не найдена"
text={Адрес «${location.pathname}» не найден в MovieBox.}
buttonText="На главную"
onClick={() => navigate('/')}
/>
</section>
);
}
**13. Файл `src/index.css` — добавь в самый конец**
Существующие стили не удаляй.
css
.nav-item.active {
color: var(--accent);
background: var(--surface-2);
}
.genre-btn {
cursor: pointer;
}
.genre-btn.active {
color: #111;
background: var(--accent);
border-color: var(--accent);
}
.search-submit {
border: 0;
background: transparent;
color: var(--accent);
cursor: pointer;
}
.search:focus-within {
border-color: var(--accent);
}
@media (max-width: 700px) {
.header-inner {
flex-wrap: wrap;
gap: 12px;
padding-top: 12px;
padding-bottom: 12px;
}
.nav {
order: 3;
width: 100%;
flex-wrap: wrap;
}
.search {
flex: 1;
min-width: 120px;
}
}
**14. Проверь работу**
Запусти в терминале:
bash
npm run dev -- --host 0.0.0.0
В Codespaces открой вкладку **Ports**, затем адрес порта **5173**.
Проверь:
* «Открыть каталог» показывает все шесть фильмов.
* Выбор жанра меняет адрес и список фильмов.
* После обновления страницы выбранный жанр сохраняется.
* Нажатие на карточку открывает именно выбранный фильм.
* Кнопка «Назад» возвращает к предыдущей странице.
* Поиск `Начало` или `Interstellar` находит фильм.
* Адрес `/unknown` показывает страницу 404.
Для проверки готовности к публикации выполни:
bash
npm run build
`