Загрузка данных


<!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
`