Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 915c3dba11 | |||
| 13ea8afc38 | |||
| c6cb30ae37 | |||
|
|
a88039abc6 | ||
|
|
9a980379a2 | ||
|
|
e3a1b6f03c | ||
| bd75372ea2 | |||
| 172a6c51da | |||
| 5d7d72cc31 | |||
| ec6e824bbb | |||
| 00abf69d77 |
32
.eslintrc.js
Normal file
32
.eslintrc.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export default {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'plugin:prettier/recommended'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
},
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module'
|
||||||
|
},
|
||||||
|
plugins: ['react', 'react-hooks', 'prettier'],
|
||||||
|
rules: {
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'prettier/prettier': 'warn',
|
||||||
|
'no-unused-vars': 'warn'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
||||||
73
about.html
73
about.html
@@ -1,73 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>О фильме - Груз 200</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li><a href="index.html">Главная</a></li>
|
|
||||||
<li><a href="catalog.html">Каталог</a></li>
|
|
||||||
<li><a href="films.html">Фильмы</a></li>
|
|
||||||
<li><a href="seriales.html">Сериалы</a></li>
|
|
||||||
<li><a href="reviews.html">Рецензии</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<div class="logo">
|
|
||||||
<img src="resources/logo.webp" alt="Online Cinema Theater Logo">
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<section class="movie-details">
|
|
||||||
<div class="movie-poster">
|
|
||||||
<img src="resources/movies/gruz.jpeg" alt="Movie Poster">
|
|
||||||
</div>
|
|
||||||
<div class="movie-info">
|
|
||||||
<h1>Груз 200</h1>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th class="header-th">Продолжительность</th>
|
|
||||||
<td>1 час 50 минут</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="header-th">Год выпуска</th>
|
|
||||||
<td>2007</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="header-th">Страна</th>
|
|
||||||
<td>Россия</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="header-th">Режиссер</th>
|
|
||||||
<td>Алексей Балабанов</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="header-th">Жанр</th>
|
|
||||||
<td>триллер, драма, криминал</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<br>
|
|
||||||
<p>Описание:</p>
|
|
||||||
<p>
|
|
||||||
Груз 200 - это история о двух братьях, которые вместе с другими людьми отправляются в грузовике на
|
|
||||||
поиски золота в Африке.
|
|
||||||
Однако путь к цели оказывается опасным и сложным, и братья сталкиваются с различными препятствиями, в
|
|
||||||
том числе с коррупцией и жестокостью.
|
|
||||||
</p>
|
|
||||||
<iframe width="560" height="315"
|
|
||||||
src="https://www.youtube.com/embed/dQw4w9WgXcQ?si=iaZ0q33EJFBzeIZ_?autoplay=1"
|
|
||||||
title="YouTube video player" frameborder="0"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
||||||
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<p>© 2022 Online Cinema Theater. All rights reserved.</p>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
53
catalog.html
53
catalog.html
@@ -1,53 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Online Cinema Theater - Catalog</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li><a href="index.html">Главная</a></li>
|
|
||||||
<li><a href="catalog.html">Каталог</a></li>
|
|
||||||
<li><a href="films.html">Фильмы</a></li>
|
|
||||||
<li><a href="seriales.html">Сериалы</a></li>
|
|
||||||
<li><a href="reviews.html">Рецензии</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<div class="logo">
|
|
||||||
<img src="resources/logo.webp" alt="Online Cinema Theater Logo">
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<section class="movie-catalog">
|
|
||||||
<h2>Каталог фильмов</h2>
|
|
||||||
<div class="genre-filter">
|
|
||||||
<label for="genre-select">Выберите жанр:</label>
|
|
||||||
<select id="genre-select">
|
|
||||||
<option value="all">Все жанры</option>
|
|
||||||
<option value="action">Боевик</option>
|
|
||||||
<option value="comedy">Комедия</option>
|
|
||||||
<option value="drama">Драма</option>
|
|
||||||
<option value="crime">Криминал</option>
|
|
||||||
<option value="thriller">Триллер</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="movie-list">
|
|
||||||
<div class="movie-card">
|
|
||||||
<img src="resources/movies/gruz.jpeg" alt="Movie Poster">
|
|
||||||
<h3>Груз 200</h3>
|
|
||||||
<p>Режиссер: Алексей Балабанов</p>
|
|
||||||
<p>Жанр: триллер драма криминал</p>
|
|
||||||
<p>Год выпуска: 2007</p>
|
|
||||||
<a href="about.html" class="watch-now-btn">Смотреть сейчас</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<p>© 2022 Online Cinema Theater. All rights reserved.</p>
|
|
||||||
</footer></body>
|
|
||||||
</html>
|
|
||||||
@@ -16,4 +16,4 @@
|
|||||||
|
|
||||||
Рецензии
|
Рецензии
|
||||||
Назначение:
|
Назначение:
|
||||||
Страница рецензий предоставляет пользователям возможность ознакомиться с профессиональными и пользовательскими отзывами о фильмах и сериалах. Здесь публикуются обзоры, аналитические статьи и оценки, которые помогут посетителям сделать выбор перед просмотром. Рецензии могут сопровождаться комментариями, рейтингами и ссылками на детальные обзоры, а также предоставлять возможность сортировки по жанрам, дате публикации или популярности обзора.
|
Страница рецензий предоставляет пользователям возможность ознакомиться с профессиональными и пользовательскими отзывами о фильмах и сериалах. Здесь публикуются обзоры, аналитические Что глянуть? и оценки, которые помогут посетителям сделать выбор перед просмотром. Рецензии могут сопровождаться комментариями, рейтингами и ссылками на детальные обзоры, а также предоставлять возможность сортировки по жанрам, дате публикации или популярности обзора.
|
||||||
58
films.html
58
films.html
@@ -1,58 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Online Cinema Theater</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li><a href="index.html">Главная</a></li>
|
|
||||||
<li><a href="catalog.html">Каталог</a></li>
|
|
||||||
<li><a href="films.html">Фильмы</a></li>
|
|
||||||
<li><a href="seriales.html">Сериалы</a></li>
|
|
||||||
<li><a href="reviews.html">Рецензии</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<div class="logo">
|
|
||||||
<img src="resources/logo.webp" alt="Online Cinema Theater Logo">
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<section class="featured-movies">
|
|
||||||
<h2>Оновные фильмы</h2>
|
|
||||||
<div class="movie-list">
|
|
||||||
<div class="movie-card">
|
|
||||||
<img src="resources/movies/gruz.jpeg" alt="Movie Poster">
|
|
||||||
<h3>Груз 200</h3>
|
|
||||||
<p>Режиссер: Алексей Балабанов</p>
|
|
||||||
<p>Жанр: триллер драма криминал</p>
|
|
||||||
<p>Год выпуска: 2007</p>
|
|
||||||
<a href="about.html" class="watch-now-btn">Смотреть сейчас</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="upcoming-movies">
|
|
||||||
<h2>Предтоящие фильмы</h2>
|
|
||||||
<div class="movie-list">
|
|
||||||
<div class="movie-list">
|
|
||||||
<div class="movie-card">
|
|
||||||
<img src="resources/movies/gruz.jpeg" alt="Movie Poster">
|
|
||||||
<h3>Груз 200</h3>
|
|
||||||
<p>Режиссер: Алексей Балабанов</p>
|
|
||||||
<p>Жанр: триллер драма криминал</p>
|
|
||||||
<p>Год выпуска: 2025</p>
|
|
||||||
<a href="about.html" class="watch-now-btn">Смотреть сейчас</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<p>© 2022 Online Cinema Theater. All rights reserved.</p>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
58
index.html
58
index.html
@@ -1,58 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Online Cinema Theater</title>
|
<title>Кинотеатр - React SPA</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<div id="root"></div>
|
||||||
<nav>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
<ul>
|
|
||||||
<li><a href="index.html">Главная</a></li>
|
|
||||||
<li><a href="catalog.html">Каталог</a></li>
|
|
||||||
<li><a href="films.html">Фильмы</a></li>
|
|
||||||
<li><a href="seriales.html">Сериалы</a></li>
|
|
||||||
<li><a href="reviews.html">Рецензии</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<div class="logo">
|
|
||||||
<img src="resources/logo.webp" alt="Online Cinema Theater Logo">
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<section class="featured-movies">
|
|
||||||
<h2>Оновные фильмы</h2>
|
|
||||||
<div class="movie-list">
|
|
||||||
<div class="movie-card">
|
|
||||||
<img src="resources/movies/gruz.jpeg" alt="Movie Poster">
|
|
||||||
<h3>Груз 200</h3>
|
|
||||||
<p>Режиссер: Алексей Балабанов</p>
|
|
||||||
<p>Жанр: триллер драма криминал</p>
|
|
||||||
<p>Год выпуска: 2007</p>
|
|
||||||
<a href="about.html" class="watch-now-btn">Смотреть сейчас</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="upcoming-movies">
|
|
||||||
<h2>Предтоящие фильмы</h2>
|
|
||||||
<div class="movie-list">
|
|
||||||
<div class="movie-list">
|
|
||||||
<div class="movie-card">
|
|
||||||
<img src="resources/movies/gruz.jpeg" alt="Movie Poster">
|
|
||||||
<h3>Груз 200</h3>
|
|
||||||
<p>Режиссер: Алексей Балабанов</p>
|
|
||||||
<p>Жанр: триллер драма криминал</p>
|
|
||||||
<p>Год выпуска: 2025</p>
|
|
||||||
<a href="about.html" class="watch-now-btn">Смотреть сейчас</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<p>© 2022 Online Cinema Theater. All rights reserved.</p>
|
|
||||||
</footer>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
6082
package-lock.json
generated
Normal file
6082
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "pibd-24_boiko_m.s._internetprogramming",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --ext .js,.jsx",
|
||||||
|
"lint:fix": "eslint . --ext .js,.jsx --fix",
|
||||||
|
"format": "prettier --write \"**/*.{js,jsx,html,css,json}\"",
|
||||||
|
"server": "json-server --watch db.json --port 3000"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.is.ulstu.ru/LivelyPuer/PIbd-24_Boiko_M.S._InternetProgramming.git"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-prettier": "^10.1.1",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"json-server": "^0.17.4",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"vite": "^5.2.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^5.3.3",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.22.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
postcss.config.js
Normal file
3
postcss.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
plugins: []
|
||||||
|
};
|
||||||
3
resources/icons/icq.svg
Normal file
3
resources/icons/icq.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg width="210mm" height="210mm" version="1.1" viewBox="0 0 210 210" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="13.229"><g fill="#00ff03"><path d="m106.61 110.23s-42.218-47.933-48.752-62.93c-6.534-14.998-4.4796-30.925 7.3807-35.35 11.86-4.4252 29.058 10.284 32.436 29.134 3.3782 18.85 8.9346 69.146 8.9346 69.146z"/><path d="m104.24 108.08s-3.4772-58.275 0-77.208c3.4772-18.933 21.955-27.771 36.631-23.482 14.676 4.2897 26.361 23.591 15.968 42.267-10.394 18.676-52.599 58.422-52.599 58.422z"/><path d="m104.14 106.37s40.123-44.003 58.085-48.769c17.962-4.7668 29.582-2.6383 34.339 9.6808 4.7576 12.319-7.0663 24.257-24.476 31.234-17.41 6.977-67.948 7.8542-67.948 7.8542z"/><path d="m103.95 105.45s64.011-10.41 80.734-0.91329c16.724 9.4971 17.563 19.84 16.256 31.234s-7.5384 23.693-28.129 23.197-68.862-53.518-68.862-53.518z"/><path d="m103.38 103.59s33.695 33.474 41.812 47.95c8.1173 14.476 5.3388 30.4-1.7262 35.866s-18.627 2.975-30.496-11.508c-11.869-14.483-9.59-72.308-9.59-72.308z"/><path d="m104.47 106.23s9.8769 64.267 0.54797 79.273c-9.329 15.006-22.579 21.054-35.001 17.249s-20.905-12.366-19.065-32.592c1.8397-20.226 53.518-63.93 53.518-63.93z"/></g><path d="m103.44 107.4s-35.939 37.331-51.327 40.732c-15.388 3.4011-23.473 2.9458-28.129-4.9317s-5.067-16.321 7.6716-27.216 71.784-8.5849 71.784-8.5849z" fill="#f5091f"/><path d="m102.01 107.02s-49.266 2.8098-69.592 0-24.215-11.647-23.745-23.745c0.46999-12.098 15.745-27.854 33.426-27.033s59.911 50.779 59.911 50.779z" fill="#00ff03"/><circle cx="103.56" cy="104.51" r="22.852" fill="#f8ee3e"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
resources/icons/tg.webp
Normal file
BIN
resources/icons/tg.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
BIN
resources/icons/vk.webp
Normal file
BIN
resources/icons/vk.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
resources/movies/brat.webp
Normal file
BIN
resources/movies/brat.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
BIN
resources/movies/slonik.jpg
Normal file
BIN
resources/movies/slonik.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
41
reviews.html
41
reviews.html
@@ -1,41 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Рецензии</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li><a href="index.html">Главная</a></li>
|
|
||||||
<li><a href="catalog.html">Каталог</a></li>
|
|
||||||
<li><a href="films.html">Фильмы</a></li>
|
|
||||||
<li><a href="seriales.html">Сериалы</a></li>
|
|
||||||
<li><a href="reviews.html">Рецензии</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<div class="logo">
|
|
||||||
<img src="resources/logo.webp" alt="Online Cinema Theater Logo">
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<section class="movie-reviews">
|
|
||||||
<article>
|
|
||||||
<h2>Груз 200</h2>
|
|
||||||
<img src="resources/movies/gruz.jpeg" alt="Movie Poster">
|
|
||||||
<p>
|
|
||||||
Рецензия: Груз 200 - это история о двух братьях, которые вместе с другими людьми отправляются в грузовике на поиски золота в Африке.
|
|
||||||
Однако путь к цели оказывается опасным и сложным, и братья сталкиваются с различными препятствиями, в том числе с коррупцией и жестокостью.
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<p>© 2022 Online Cinema Theater. All rights reserved.</p>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Online Cinema Theater</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li><a href="index.html">Главная</a></li>
|
|
||||||
<li><a href="catalog.html">Каталог</a></li>
|
|
||||||
<li><a href="films.html">Фильмы</a></li>
|
|
||||||
<li><a href="seriales.html">Сериалы</a></li>
|
|
||||||
<li><a href="reviews.html">Рецензии</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<div class="logo">
|
|
||||||
<img src="resources/logo.webp" alt="Online Cinema Theater Logo">
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<section class="featured-movies">
|
|
||||||
<h2>Оновные фильмы</h2>
|
|
||||||
<div class="movie-list">
|
|
||||||
<div class="movie-card">
|
|
||||||
<img src="resources/series/960.webp" alt="Movie Poster">
|
|
||||||
<h3>Сериал: Преступление и наказание</h3>
|
|
||||||
<p>Год выпуска: 2007</p>
|
|
||||||
<a href="about.html" class="watch-now-btn">Смотреть сейчас</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<p>© 2022 Online Cinema Theater. All rights reserved.</p>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
82
src/App.jsx
Normal file
82
src/App.jsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Routes, Route, Link } from 'react-router-dom';
|
||||||
|
import HomePage from './pages/HomePage';
|
||||||
|
import CatalogPage from './pages/CatalogPage';
|
||||||
|
import AddMoviePage from './pages/AddMoviePage';
|
||||||
|
import EditMoviePage from './pages/EditMoviePage';
|
||||||
|
import AboutPage from './pages/AboutPage';
|
||||||
|
import MovieDetailsPage from './pages/MovieDetailsPage';
|
||||||
|
import FavoritesPage from './pages/FavoritesPage';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div className="container">
|
||||||
|
<Link className="navbar-brand" to="/">
|
||||||
|
<i className="bi bi-film text-orange me-2"></i>
|
||||||
|
Кинотеатр
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
className="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbarNav"
|
||||||
|
aria-controls="navbarNav"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<span className="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div className="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link className="nav-link" to="/">Главная</Link>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link className="nav-link" to="/catalog">Каталог</Link>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link className="nav-link" to="/favorites">Избранное</Link>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link className="nav-link" to="/about">О нас</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Link to="/add-movie" className="btn btn-orange">
|
||||||
|
<i className="bi bi-plus-circle me-1"></i>Добавить фильм
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="container py-4">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/catalog" element={<CatalogPage />} />
|
||||||
|
<Route path="/add-movie" element={<AddMoviePage />} />
|
||||||
|
<Route path="/edit-movie/:id" element={<EditMoviePage />} />
|
||||||
|
<Route path="/movie/:id" element={<MovieDetailsPage />} />
|
||||||
|
<Route path="/favorites" element={<FavoritesPage />} />
|
||||||
|
<Route path="/about" element={<AboutPage />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="bg-dark text-white py-4">
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<h5>Кинотеатр</h5>
|
||||||
|
<p>Лучшие фильмы и сериалы в одном месте</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6 text-md-end">
|
||||||
|
<p>© 2023 Кинотеатр. Все права защищены.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
75
src/components/movie/MovieCard.jsx
Normal file
75
src/components/movie/MovieCard.jsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import useFavorites from '../../hooks/useFavorites';
|
||||||
|
|
||||||
|
function MovieCard({ movie, onDelete }) {
|
||||||
|
const { addFavorite, removeFavorite, isFavorite } = useFavorites();
|
||||||
|
const isFav = isFavorite(movie.id);
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (window.confirm('Вы уверены, что хотите удалить этот фильм?')) {
|
||||||
|
onDelete(movie.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleFavorite = () => {
|
||||||
|
if (isFav) {
|
||||||
|
removeFavorite(movie.id);
|
||||||
|
} else {
|
||||||
|
addFavorite(movie.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card movie-card h-100 bg-dark p-0">
|
||||||
|
<img src={movie.poster} className="card-img-top" alt={`${movie.title} Poster`} />
|
||||||
|
<div className="card-body">
|
||||||
|
<h5 className="card-title text-white">
|
||||||
|
<i className="bi bi-film text-orange me-2"></i>{movie.title}
|
||||||
|
</h5>
|
||||||
|
<p className="card-text text-white">
|
||||||
|
<i className="bi bi-person-video3 text-secondary me-2"></i>{movie.director}
|
||||||
|
</p>
|
||||||
|
<p className="card-text text-light">
|
||||||
|
<i className="bi bi-tags text-secondary me-2"></i>
|
||||||
|
{Array.isArray(movie.genres) ? movie.genres.join(', ') : movie.genres}
|
||||||
|
</p>
|
||||||
|
<p className="card-text text-light">
|
||||||
|
<i className="bi bi-calendar3 text-secondary me-2"></i>{movie.year}
|
||||||
|
</p>
|
||||||
|
{movie.description && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<p className="card-text text-light small">
|
||||||
|
<i className="bi bi-text-paragraph text-secondary me-2"></i>
|
||||||
|
{movie.description.length > 100
|
||||||
|
? movie.description.substring(0, 100) + '...'
|
||||||
|
: movie.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="card-footer d-flex justify-content-between align-items-center">
|
||||||
|
<Link to={`/movie/${movie.id}`} className="btn btn-orange">
|
||||||
|
<i className="bi bi-play-circle me-1"></i>Смотреть
|
||||||
|
</Link>
|
||||||
|
<div className='d-flex align-items-center'>
|
||||||
|
<button
|
||||||
|
onClick={handleToggleFavorite}
|
||||||
|
className={`btn ${isFav ? 'btn-danger' : 'btn-outline-danger'} me-1`}
|
||||||
|
title={isFav ? 'Удалить из избранного' : 'Добавить в избранное'}
|
||||||
|
>
|
||||||
|
<i className={`bi ${isFav ? 'bi-heart-fill' : 'bi-heart'}`}></i>
|
||||||
|
</button>
|
||||||
|
<Link to={`/edit-movie/${movie.id}`} className="btn btn-outline-warning edit-movie me-1">
|
||||||
|
<i className="bi bi-pencil"></i>
|
||||||
|
</Link>
|
||||||
|
<button className="btn btn-outline-danger delete-movie" onClick={handleDelete}>
|
||||||
|
<i className="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCard;
|
||||||
129
src/components/movie/MovieForm.jsx
Normal file
129
src/components/movie/MovieForm.jsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import useMovieForm from '../../hooks/useMovieForm';
|
||||||
|
|
||||||
|
function MovieForm({ movie, onSubmit, isEditing = false }) {
|
||||||
|
const {
|
||||||
|
title, setTitle,
|
||||||
|
director, setDirector,
|
||||||
|
genres, handleGenreChange,
|
||||||
|
year, setYear,
|
||||||
|
description, setDescription,
|
||||||
|
poster, handlePosterChange,
|
||||||
|
previewVisible,
|
||||||
|
getFormData
|
||||||
|
} = useMovieForm(movie);
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit(getFormData());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="movieTitle" className="form-label">Название фильма</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="movieTitle"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="movieDirector" className="form-label">Режиссер</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="movieDirector"
|
||||||
|
value={director}
|
||||||
|
onChange={(e) => setDirector(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="movieGenre" className="form-label">Жанры</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
id="movieGenre"
|
||||||
|
multiple
|
||||||
|
value={genres}
|
||||||
|
onChange={handleGenreChange}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="Боевик">Боевик</option>
|
||||||
|
<option value="Комедия">Комедия</option>
|
||||||
|
<option value="Драма">Драма</option>
|
||||||
|
<option value="Фантастика">Фантастика</option>
|
||||||
|
<option value="Ужасы">Ужасы</option>
|
||||||
|
<option value="Триллер">Триллер</option>
|
||||||
|
<option value="Детектив">Детектив</option>
|
||||||
|
<option value="Приключения">Приключения</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="movieYear" className="form-label">Год выпуска</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-control"
|
||||||
|
id="movieYear"
|
||||||
|
min="1900"
|
||||||
|
max={new Date().getFullYear()}
|
||||||
|
value={year}
|
||||||
|
onChange={(e) => setYear(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="movieDescription" className="form-label">Описание</label>
|
||||||
|
<textarea
|
||||||
|
className="form-control"
|
||||||
|
id="movieDescription"
|
||||||
|
rows="3"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="moviePoster" className="form-label">Постер</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="form-control"
|
||||||
|
id="moviePoster"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handlePosterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{previewVisible && (
|
||||||
|
<div className="preview-container mb-3">
|
||||||
|
<label className="form-label">Предпросмотр постера</label>
|
||||||
|
<img
|
||||||
|
src={poster}
|
||||||
|
alt="Предпросмотр постера"
|
||||||
|
id="posterPreview"
|
||||||
|
className="img-thumbnail"
|
||||||
|
style={{ maxHeight: '300px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="d-flex justify-content-between">
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
{isEditing ? 'Сохранить изменения' : 'Добавить фильм'}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => window.history.back()}>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieForm;
|
||||||
45
src/components/movie/MovieList.jsx
Normal file
45
src/components/movie/MovieList.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MovieCard from './MovieCard';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function MovieList({ movies, onDeleteMovie, isHomepage = false }) {
|
||||||
|
// If we're on the homepage, only show up to 6 featured movies
|
||||||
|
const moviesToShow = isHomepage ? movies.slice(0, 6) : movies;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 d-flex justify-content-between align-items-center">
|
||||||
|
<h2 className={isHomepage ? "text-orange" : ""}>
|
||||||
|
{isHomepage ? 'Популярные фильмы' : 'Каталог фильмов'}
|
||||||
|
</h2>
|
||||||
|
<Link to="/add-movie" className="btn btn-success">
|
||||||
|
<i className="bi bi-plus-circle me-2"></i>Добавить фильм
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" id="movieContainer">
|
||||||
|
{moviesToShow.length > 0 ? (
|
||||||
|
moviesToShow.map(movie => (
|
||||||
|
<div className="col" key={movie.id}>
|
||||||
|
<MovieCard movie={movie} onDelete={onDeleteMovie} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="col-12 text-center py-5">
|
||||||
|
<p className="text-muted">Фильмы не найдены</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isHomepage && movies.length > 6 && (
|
||||||
|
<div className="col-12 text-center mt-4">
|
||||||
|
<Link to="/catalog" className="btn btn-primary">
|
||||||
|
Смотреть все фильмы
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieList;
|
||||||
45
src/hooks/useFavorites.js
Normal file
45
src/hooks/useFavorites.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
const FAVORITES_KEY = 'favoriteMovies';
|
||||||
|
|
||||||
|
function useFavorites() {
|
||||||
|
const [favoriteIds, setFavoriteIds] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedFavorites = localStorage.getItem(FAVORITES_KEY);
|
||||||
|
if (storedFavorites) {
|
||||||
|
setFavoriteIds(JSON.parse(storedFavorites));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateLocalStorage = (ids) => {
|
||||||
|
localStorage.setItem(FAVORITES_KEY, JSON.stringify(ids));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addFavorite = useCallback((movieId) => {
|
||||||
|
setFavoriteIds((prevIds) => {
|
||||||
|
if (!prevIds.includes(movieId)) {
|
||||||
|
const newIds = [...prevIds, movieId];
|
||||||
|
updateLocalStorage(newIds);
|
||||||
|
return newIds;
|
||||||
|
}
|
||||||
|
return prevIds;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeFavorite = useCallback((movieId) => {
|
||||||
|
setFavoriteIds((prevIds) => {
|
||||||
|
const newIds = prevIds.filter(id => id !== movieId);
|
||||||
|
updateLocalStorage(newIds);
|
||||||
|
return newIds;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isFavorite = useCallback((movieId) => {
|
||||||
|
return favoriteIds.includes(movieId);
|
||||||
|
}, [favoriteIds]);
|
||||||
|
|
||||||
|
return { favoriteIds, addFavorite, removeFavorite, isFavorite };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFavorites;
|
||||||
47
src/hooks/useMovie.js
Normal file
47
src/hooks/useMovie.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import MovieService from '../services/MovieService';
|
||||||
|
|
||||||
|
function useMovie(id) {
|
||||||
|
const [movie, setMovie] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) {
|
||||||
|
setLoading(false);
|
||||||
|
setError('Movie ID is not provided.'); // Or handle as you see fit
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchMovie = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await MovieService.getMovieById(id);
|
||||||
|
if (!data) {
|
||||||
|
setError('Фильм не найден');
|
||||||
|
setMovie(null); // Ensure movie state is reset if not found
|
||||||
|
} else {
|
||||||
|
setMovie(data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching movie:', err);
|
||||||
|
setError('Не удалось загрузить данные фильма');
|
||||||
|
setMovie(null); // Ensure movie state is reset on error
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMovie();
|
||||||
|
}, [id]); // Effect runs when the id changes
|
||||||
|
|
||||||
|
return {
|
||||||
|
movie,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
setMovie // It might be useful to allow manually setting the movie, e.g., after an update
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMovie;
|
||||||
80
src/hooks/useMovieForm.js
Normal file
80
src/hooks/useMovieForm.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
function useMovieForm(initialMovieData = null) {
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [director, setDirector] = useState('');
|
||||||
|
const [genres, setGenres] = useState([]);
|
||||||
|
const [year, setYear] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [poster, setPoster] = useState('');
|
||||||
|
const [previewVisible, setPreviewVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialMovieData) {
|
||||||
|
setTitle(initialMovieData.title || '');
|
||||||
|
setDirector(initialMovieData.director || '');
|
||||||
|
setGenres(Array.isArray(initialMovieData.genres) ? initialMovieData.genres : []);
|
||||||
|
setYear(initialMovieData.year || '');
|
||||||
|
setDescription(initialMovieData.description || '');
|
||||||
|
setPoster(initialMovieData.poster || '');
|
||||||
|
if (initialMovieData.poster) {
|
||||||
|
setPreviewVisible(true);
|
||||||
|
} else {
|
||||||
|
setPreviewVisible(false); // Ensure preview is hidden if no poster
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reset form if no initial data (e.g., for add form after an edit)
|
||||||
|
setTitle('');
|
||||||
|
setDirector('');
|
||||||
|
setGenres([]);
|
||||||
|
setYear('');
|
||||||
|
setDescription('');
|
||||||
|
setPoster('');
|
||||||
|
setPreviewVisible(false);
|
||||||
|
}
|
||||||
|
}, [initialMovieData]);
|
||||||
|
|
||||||
|
const handleGenreChange = (e) => {
|
||||||
|
const selectedGenres = Array.from(e.target.selectedOptions).map(option => option.value);
|
||||||
|
setGenres(selectedGenres);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePosterChange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
setPoster(event.target.result);
|
||||||
|
setPreviewVisible(true);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
} else {
|
||||||
|
// If no file is selected, or selection is cancelled
|
||||||
|
// setPoster(''); // Optionally clear poster if no file is chosen
|
||||||
|
// setPreviewVisible(false); // Optionally hide preview
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFormData = () => ({
|
||||||
|
title,
|
||||||
|
director,
|
||||||
|
genres,
|
||||||
|
year,
|
||||||
|
description,
|
||||||
|
poster
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exposed state and handlers
|
||||||
|
return {
|
||||||
|
title, setTitle,
|
||||||
|
director, setDirector,
|
||||||
|
genres, setGenres, handleGenreChange, // Expose specific handler for genres
|
||||||
|
year, setYear,
|
||||||
|
description, setDescription,
|
||||||
|
poster, setPoster, handlePosterChange, // Expose specific handler for poster
|
||||||
|
previewVisible,
|
||||||
|
getFormData // Function to get all form data for submission
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMovieForm;
|
||||||
91
src/hooks/useMovies.js
Normal file
91
src/hooks/useMovies.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import MovieService from '../services/MovieService';
|
||||||
|
import useFavorites from './useFavorites'; // Import useFavorites
|
||||||
|
|
||||||
|
function useMovies() {
|
||||||
|
const [allMovies, setAllMovies] = useState([]); // Renamed from movies to allMovies
|
||||||
|
const [filteredMovies, setFilteredMovies] = useState([]); // This will be the final list to display
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [selectedGenre, setSelectedGenre] = useState('');
|
||||||
|
const [genres, setGenres] = useState([]);
|
||||||
|
const { favoriteIds } = useFavorites(); // Get favorite IDs
|
||||||
|
const [showOnlyFavorites, setShowOnlyFavorites] = useState(false); // New state for favorites filter
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchMoviesAndGenres = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await MovieService.getMovies();
|
||||||
|
setAllMovies(data);
|
||||||
|
// setFilteredMovies(data); // Initial filtering will happen in the next useEffect
|
||||||
|
|
||||||
|
const allGenres = data.flatMap(movie => movie.genres || []).filter(genre => genre.trim() !== '');
|
||||||
|
const uniqueGenres = [...new Set(allGenres)].sort();
|
||||||
|
setGenres(uniqueGenres);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching movies:', err);
|
||||||
|
setError('Не удалось загрузить фильмы. Пожалуйста, попробуйте позже.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMoviesAndGenres();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
let tempMovies = [...allMovies];
|
||||||
|
|
||||||
|
if (showOnlyFavorites) {
|
||||||
|
tempMovies = tempMovies.filter(movie => favoriteIds.includes(movie.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedGenre) {
|
||||||
|
tempMovies = tempMovies.filter(movie =>
|
||||||
|
(Array.isArray(movie.genres) && movie.genres.includes(selectedGenre)) ||
|
||||||
|
(typeof movie.genres === 'string' && movie.genres === selectedGenre)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
tempMovies = tempMovies.filter(movie =>
|
||||||
|
movie.title.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setFilteredMovies(tempMovies);
|
||||||
|
setLoading(false);
|
||||||
|
}, [searchTerm, selectedGenre, allMovies, favoriteIds, showOnlyFavorites]); // Add dependencies
|
||||||
|
|
||||||
|
|
||||||
|
const handleDeleteMovie = async (id) => {
|
||||||
|
try {
|
||||||
|
await MovieService.deleteMovie(id);
|
||||||
|
setAllMovies(prevMovies => prevMovies.filter(movie => movie.id !== id));
|
||||||
|
// No need to setFilteredMovies here, the useEffect above will handle it
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting movie:', err);
|
||||||
|
setError('Не удалось удалить фильм.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
movies: filteredMovies, // Expose filteredMovies as movies
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
searchTerm,
|
||||||
|
setSearchTerm,
|
||||||
|
selectedGenre,
|
||||||
|
setSelectedGenre,
|
||||||
|
genres,
|
||||||
|
handleDeleteMovie,
|
||||||
|
showOnlyFavorites, // Expose new state
|
||||||
|
setShowOnlyFavorites // Expose setter for new state
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMovies;
|
||||||
17
src/main.jsx
Normal file
17
src/main.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
// Import Bootstrap
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import 'bootstrap-icons/font/bootstrap-icons.css';
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
23
src/pages/AboutPage.jsx
Normal file
23
src/pages/AboutPage.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function AboutPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-4">О нас</h2>
|
||||||
|
<div className="card bg-dark text-white">
|
||||||
|
<div className="card-body">
|
||||||
|
<h5 className="card-title">Наш кинотеатр</h5>
|
||||||
|
<p className="card-text">
|
||||||
|
Мы предлагаем широкий выбор фильмов различных жанров и направлений.
|
||||||
|
Наша цель - сделать просмотр кино максимально комфортным и приятным для вас.
|
||||||
|
</p>
|
||||||
|
<p className="card-text">
|
||||||
|
Наслаждайтесь просмотром любимых фильмов в отличном качестве!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AboutPage;
|
||||||
28
src/pages/AddMoviePage.jsx
Normal file
28
src/pages/AddMoviePage.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import MovieForm from '../components/Movie/MovieForm';
|
||||||
|
import MovieService from '../services/MovieService';
|
||||||
|
|
||||||
|
function AddMoviePage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleAddMovie = async (movieData) => {
|
||||||
|
try {
|
||||||
|
await MovieService.addMovie(movieData);
|
||||||
|
alert('Фильм успешно добавлен!');
|
||||||
|
navigate('/catalog');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding movie:', error);
|
||||||
|
alert('Произошла ошибка при добавлении фильма.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-4">Добавить новый фильм</h2>
|
||||||
|
<MovieForm onSubmit={handleAddMovie} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddMoviePage;
|
||||||
83
src/pages/CatalogPage.jsx
Normal file
83
src/pages/CatalogPage.jsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MovieList from '../components/Movie/MovieList';
|
||||||
|
|
||||||
|
import useMovies from '../hooks/useMovies';
|
||||||
|
|
||||||
|
function CatalogPage() {
|
||||||
|
const {
|
||||||
|
movies,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
searchTerm,
|
||||||
|
setSearchTerm,
|
||||||
|
selectedGenre,
|
||||||
|
setSelectedGenre,
|
||||||
|
genres,
|
||||||
|
handleDeleteMovie,
|
||||||
|
showOnlyFavorites,
|
||||||
|
setShowOnlyFavorites
|
||||||
|
} = useMovies();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (loading && movies.length === 0) {
|
||||||
|
return <div className="text-center py-5"><div className="spinner-border text-orange" role="status"></div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="container mt-3"><div className="alert alert-danger">{error}</div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-3">
|
||||||
|
<h1 className="mb-4 text-orange">Каталог фильмов</h1>
|
||||||
|
|
||||||
|
{/* Filter Controls */}
|
||||||
|
<div className="row mb-4 g-3 align-items-center">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Поиск по названию..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-4">
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={selectedGenre}
|
||||||
|
onChange={(e) => setSelectedGenre(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Все жанры</option>
|
||||||
|
{genres.map(genre => (
|
||||||
|
<option key={genre} value={genre}>{genre}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-2">
|
||||||
|
<div className="form-check form-switch">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id="favoritesSwitch"
|
||||||
|
checked={showOnlyFavorites}
|
||||||
|
onChange={(e) => setShowOnlyFavorites(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor="favoritesSwitch">Избранное</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Movie List */}
|
||||||
|
{movies.length > 0 ? (
|
||||||
|
<MovieList movies={movies} onDelete={handleDeleteMovie} />
|
||||||
|
) : (
|
||||||
|
!loading && <p>Фильмы не найдены. Попробуйте изменить критерии поиска.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CatalogPage;
|
||||||
43
src/pages/EditMoviePage.jsx
Normal file
43
src/pages/EditMoviePage.jsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import MovieForm from '../components/Movie/MovieForm';
|
||||||
|
import MovieService from '../services/MovieService';
|
||||||
|
import useMovie from '../hooks/useMovie';
|
||||||
|
|
||||||
|
function EditMoviePage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { movie, loading, error, setMovie } = useMovie(id);
|
||||||
|
|
||||||
|
const handleUpdateMovie = async (movieData) => {
|
||||||
|
try {
|
||||||
|
const updatedMovie = await MovieService.updateMovie(id, movieData);
|
||||||
|
alert('Фильм успешно обновлен!');
|
||||||
|
navigate('/catalog');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating movie:', err);
|
||||||
|
alert('Произошла ошибка при обновлении фильма.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="text-center py-5"><div className="spinner-border" role="status"></div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="alert alert-danger">{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!movie) {
|
||||||
|
return <div className="alert alert-warning">Фильм для редактирования не найден.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-4">Редактировать фильм</h2>
|
||||||
|
<MovieForm movie={movie} onSubmit={handleUpdateMovie} isEditing={true} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditMoviePage;
|
||||||
60
src/pages/FavoritesPage.jsx
Normal file
60
src/pages/FavoritesPage.jsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import MovieList from '../components/Movie/MovieList';
|
||||||
|
import MovieService from '../services/MovieService';
|
||||||
|
import useFavorites from '../hooks/useFavorites';
|
||||||
|
|
||||||
|
function FavoritesPage() {
|
||||||
|
const { favoriteIds, removeFavorite } = useFavorites();
|
||||||
|
const [favoriteMovies, setFavoriteMovies] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchFavoriteMovies = async () => {
|
||||||
|
if (favoriteIds.length === 0) {
|
||||||
|
setFavoriteMovies([]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const allMovies = await MovieService.getMovies();
|
||||||
|
const favs = allMovies.filter(movie => favoriteIds.includes(movie.id));
|
||||||
|
setFavoriteMovies(favs);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching favorite movies:', err);
|
||||||
|
setError('Не удалось загрузить избранные фильмы.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchFavoriteMovies();
|
||||||
|
}, [favoriteIds]);
|
||||||
|
|
||||||
|
const handleDeleteFromFavorites = (movieId) => {
|
||||||
|
removeFavorite(movieId);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="text-center py-5"><div className="spinner-border" role="status"></div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="container mt-3"><div className="alert alert-danger">{error}</div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-3">
|
||||||
|
<h1 className="mb-4 text-orange">Избранные фильмы</h1>
|
||||||
|
{favoriteMovies.length > 0 ? (
|
||||||
|
<MovieList movies={favoriteMovies} onDelete={handleDeleteFromFavorites} />
|
||||||
|
) : (
|
||||||
|
<p>У вас пока нет избранных фильмов. Вы можете добавить их из каталога.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FavoritesPage;
|
||||||
60
src/pages/HomePage.jsx
Normal file
60
src/pages/HomePage.jsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import MovieList from '../components/Movie/MovieList';
|
||||||
|
import MovieService from '../services/MovieService';
|
||||||
|
|
||||||
|
function HomePage() {
|
||||||
|
const [movies, setMovies] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchMovies = async () => {
|
||||||
|
try {
|
||||||
|
const data = await MovieService.getMovies();
|
||||||
|
// Sort by year (newest first) for featured movies
|
||||||
|
const sortedMovies = [...data].sort((a, b) => b.year - a.year);
|
||||||
|
setMovies(sortedMovies);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching movies:', error);
|
||||||
|
setError('Не удалось загрузить фильмы. Пожалуйста, попробуйте позже.');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMovies();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteMovie = async (id) => {
|
||||||
|
try {
|
||||||
|
await MovieService.deleteMovie(id);
|
||||||
|
setMovies(movies.filter(movie => movie.id !== id));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting movie:', error);
|
||||||
|
alert('Произошла ошибка при удалении фильма.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="text-center py-5"><div className="spinner-border" role="status"></div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="alert alert-danger">{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="jumbotron bg-dark text-white p-5 mb-4 rounded">
|
||||||
|
<h1 className="display-4">Добро пожаловать в наш кинотеатр!</h1>
|
||||||
|
<p className="lead">Лучшие фильмы и сериалы в одном месте</p>
|
||||||
|
<hr className="my-4" />
|
||||||
|
<p>Смотрите новинки кино и классику в отличном качестве</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MovieList movies={movies} onDeleteMovie={handleDeleteMovie} isHomepage={true} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
61
src/pages/MovieDetailsPage.jsx
Normal file
61
src/pages/MovieDetailsPage.jsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useParams, Link } from 'react-router-dom';
|
||||||
|
import useMovie from '../hooks/useMovie';
|
||||||
|
|
||||||
|
function MovieDetailsPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const { movie, loading, error } = useMovie(id);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="text-center py-5"><div className="spinner-border" role="status"></div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="alert alert-danger">{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!movie) {
|
||||||
|
return <div className="alert alert-warning">Фильм не найден.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-5">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-4">
|
||||||
|
<img src={movie.poster} className="img-fluid rounded shadow-sm" alt={`${movie.title} Poster`} />
|
||||||
|
</div>
|
||||||
|
<div className="col-md-8">
|
||||||
|
<h1 className="mb-3 text-orange">{movie.title}</h1>
|
||||||
|
<p className="lead">
|
||||||
|
<i className="bi bi-person-video3 text-secondary me-2"></i>
|
||||||
|
<strong>Режиссер:</strong> {movie.director}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i className="bi bi-calendar3 text-secondary me-2"></i>
|
||||||
|
<strong>Год:</strong> {movie.year}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i className="bi bi-tags text-secondary me-2"></i>
|
||||||
|
<strong>Жанры:</strong> {Array.isArray(movie.genres) ? movie.genres.join(', ') : movie.genres}
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
<h5 className="mt-4 mb-3">Описание:</h5>
|
||||||
|
<p style={{ textAlign: 'justify' }}>{movie.description}</p>
|
||||||
|
<hr />
|
||||||
|
<div className="mt-4">
|
||||||
|
<Link to={`/edit-movie/${movie.id}`} className="btn btn-warning me-2">
|
||||||
|
<i className="bi bi-pencil me-1"></i> Редактировать
|
||||||
|
</Link>
|
||||||
|
<Link to="/catalog" className="btn btn-outline-secondary">
|
||||||
|
<i className="bi bi-arrow-left-circle me-1"></i> Назад к каталогу
|
||||||
|
</Link>
|
||||||
|
{/* Placeholder for a play button or video embed area */}
|
||||||
|
{/* <button className=\"btn btn-lg btn-success mt-3 w-100\">Смотреть фильм</button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieDetailsPage;
|
||||||
82
src/services/MovieService.js
Normal file
82
src/services/MovieService.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
const API_URL = 'http://localhost:3000';
|
||||||
|
|
||||||
|
export default class MovieService {
|
||||||
|
static async getMovies() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/movies`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch movies');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching movies:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getMovieById(id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/movies/${id}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch movie');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching movie:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addMovie(movie) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/movies`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(movie)
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to add movie');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding movie:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateMovie(id, movie) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/movies/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(movie)
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update movie');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating movie:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteMovie(id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/movies/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete movie');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting movie:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
150
src/style.css
Normal file
150
src/style.css
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/* Custom styles for the movie application */
|
||||||
|
.btn-orange {
|
||||||
|
background-color: #fd7e14;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-orange:hover {
|
||||||
|
background-color: #e76b00;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.text-orange {
|
||||||
|
color: #fd7e14 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redesigned movie card styling */
|
||||||
|
.movie-card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #212529 !important;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
.movie-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 16px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.movie-card .card-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
.card-img-top {
|
||||||
|
height: 380px;
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
}
|
||||||
|
.movie-card .card-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.movie-card .card-text {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.movie-card .card-footer {
|
||||||
|
background-color: rgba(0,0,0,0.1) !important;
|
||||||
|
border-top: none !important;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
}
|
||||||
|
.movie-card .btn {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for card deletion */
|
||||||
|
.movie-card.deleting {
|
||||||
|
animation: fadeOut 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from { opacity: 1; transform: scale(1); }
|
||||||
|
to { opacity: 0; transform: scale(0.8); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form styling */
|
||||||
|
.preview-container {
|
||||||
|
max-width: 300px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Jumbotron styling for homepage */
|
||||||
|
.jumbotron {
|
||||||
|
background-color: #212529;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #131516;
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global text color adjustments */
|
||||||
|
h1, h2, h3, h4, h5, h6, p, span, div, label, a {
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control, .form-select {
|
||||||
|
background-color: #212529;
|
||||||
|
color: #f8f9fa;
|
||||||
|
border-color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
background-color: #2c3034;
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add these styles for placeholder text */
|
||||||
|
.form-control::placeholder {
|
||||||
|
color: #adb5bd;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For Firefox */
|
||||||
|
.form-control::-moz-placeholder {
|
||||||
|
color: #adb5bd;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For Internet Explorer */
|
||||||
|
.form-control:-ms-input-placeholder {
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For Microsoft Edge */
|
||||||
|
.form-control::-ms-input-placeholder {
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background-color: #212529;
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #adb5bd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: #fd7e14;
|
||||||
|
}
|
||||||
140
style.css
140
style.css
@@ -1,140 +0,0 @@
|
|||||||
/* Reset default browser styles */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Open Sans', sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #121212;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #222222;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
display: flex;
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li a {
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li a:hover {
|
|
||||||
color: #ff6600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
max-width: 50px;
|
|
||||||
max-height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo img {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-movies,
|
|
||||||
.upcoming-movies {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-movies h2,
|
|
||||||
.upcoming-movies h2 {
|
|
||||||
color: #ff6600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.movie-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.movie-card {
|
|
||||||
width: 300px;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
background-color: #222222;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.movie-card img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.movie-card h3 {
|
|
||||||
color: #fff;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.movie-card p {
|
|
||||||
color: #999;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.movie-card a {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
background-color: #ff6600;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
padding: 10px 0;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.movie-card a:hover {
|
|
||||||
background-color: #fff;
|
|
||||||
color: #ff6600;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #222222;
|
|
||||||
text-align: center;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.watch-now-btn {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background-color: #ff6600;
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.watch-now-btn:hover {
|
|
||||||
background-color: #fff;
|
|
||||||
color: #ff6600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-th {
|
|
||||||
color: #fff;
|
|
||||||
text-align: left;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
18
vite.config.js
Normal file
18
vite.config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
open: true
|
||||||
|
}
|
||||||
|
});
|
||||||
BIN
~$Отчет.docx
Normal file
BIN
~$Отчет.docx
Normal file
Binary file not shown.
BIN
Отчет.docx
BIN
Отчет.docx
Binary file not shown.
Reference in New Issue
Block a user