PIbd-21_Valiulov_I.A_Labwork06 #7

Open
Ilyas wants to merge 8 commits from labwork06 into main
26 changed files with 6025 additions and 2 deletions

42
.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"tabWidth": 4,
"singleQuote": false,
"printWidth": 120,
"trailingComma": "es5",
"useTabs": false
}

View File

@@ -1,2 +1,17 @@
# 2Course-Internet-Programming Установка зависимостей
Лабораторные работы по предмету: Интернет-программирование.
```
npm install
```
Запуск в режиме разработки
```
npm start
```
Запуск для использования в продуктовой среде
```
npm run prod
```

19
db.json Normal file
View File

@@ -0,0 +1,19 @@
{
"movies": [
{
"id": "8fd8",
"title": "ittifaqq",
"poster": "https://avatars.mds.yandex.net/i?id=17b83f995e81ac7426335d0659a050263d567f34-13135934-images-thumbs&n=13"
},
{
"id": "5267",
"title": "priora",
"poster": "https://ir-3.ozone.ru/s3/multimedia-8/w1200/6738078680.jpg"
},
{
"id": "2756",
"title": "Bibiziyana",
"poster": "https://avatars.mds.yandex.net/i?id=eeaf22011f8c07976795a01b40f431b0f42c8f43-5100713-images-thumbs&n=13"
}
]
}

49
eslint.config.js Normal file
View File

@@ -0,0 +1,49 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier/flat";
import pluginImport from "eslint-plugin-import";
import reactPlugin from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
import viteConfigObj from "./vite.config.js";
export default [
{ ignores: ["dist", "vite.config.js"] },
{
files: ["**/*.{js,jsx}"],
languageOptions: {
globals: globals.browser,
},
settings: {
react: {
version: "detect",
},
"import/resolver": {
node: {
extensions: [".js", ".jsx", ".ts", ".tsx"],
},
vite: {
viteConfig: viteConfigObj,
},
},
},
plugins: {
react: reactPlugin,
"react-hooks": reactHooks,
},
},
js.configs.recommended,
pluginImport.flatConfigs.recommended,
reactRefresh.configs.recommended,
reactPlugin.configs.flat.recommended,
reactPlugin.configs.flat["jsx-runtime"],
eslintConfigPrettier,
{
rules: {
...reactHooks.configs.recommended.rules,
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"react/prop-types": ["off"],
},
},
];

39
hooks/useMovieForm.js Normal file
View File

@@ -0,0 +1,39 @@
import { useEffect, useState } from 'react';
export function useMovieForm(initialData = {}, onSubmit) {
const [formData, setFormData] = useState({
title: '',
poster: ''
});
useEffect(() => {
if (initialData) {
setFormData({
title: initialData.title || '',
poster: initialData.poster || ''
});
}
}, [initialData]);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({
...initialData,
...formData
});
};
return {
formData,
handleChange,
handleSubmit
};
}

71
hooks/useMovies.js Normal file
View File

@@ -0,0 +1,71 @@
import { useEffect, useState } from 'react';
export function useMovies() {
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchMovies = async () => {
setLoading(true);
try {
const response = await fetch('http://localhost:3001/movies');
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
setMovies(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchMovies();
}, []);
const addMovie = async (movieData) => {
try {
const response = await fetch('http://localhost:3001/movies', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(movieData),
});
const newMovie = await response.json();
setMovies(prev => [...prev, newMovie]);
return true;
} catch (err) {
setError(err.message);
return false;
}
};
const updateMovie = async (movieData) => {
try {
await fetch(`http://localhost:3001/movies/${movieData.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(movieData),
});
setMovies(prev => prev.map(m => m.id === movieData.id ? movieData : m));
return true;
} catch (err) {
setError(err.message);
return false;
}
};
const deleteMovie = async (id) => {
try {
await fetch(`http://localhost:3001/movies/${id}`, {
method: 'DELETE',
});
setMovies(prev => prev.filter(m => m.id !== id));
return true;
} catch (err) {
setError(err.message);
return false;
}
};
return { movies, loading, error, addMovie, updateMovie, deleteMovie };
}

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Онлайн кинотеатр</title>
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>

12
jsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"baseUrl": "./src/**",
"checkJs": true
},
"exclude": ["node_modules", "**/node_modules/*"]
}

5363
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "int-prog",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "vite",
"vite": "vite",
"build": "vite build",
"serve": "http-server -p 3000 ./dist/",
"prod": "npm-run-all build serve",
"lint": "eslint ."
},
"dependencies": {
"bootstrap": "^5.3.5",
"bootstrap-icons": "^1.11.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.1"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.1.1",
"eslint-import-resolver-vite": "^2.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.5",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"http-server": "^14.1.1",
"vite": "^6.2.0"
}
}

BIN
public/images/4k.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/images/film.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

BIN
report.docx Normal file

Binary file not shown.

21
src/app/App.jsx Normal file
View File

@@ -0,0 +1,21 @@
import { Route, Routes } from 'react-router-dom';
import About from '../pages/About';
import Catalog from '../pages/Catalog';
import Film from '../pages/Film';
import Home from '../pages/Home';
import Movies from '../pages/Movies';
export const App = () => {
return (
<div className="p-2">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/catalog" element={<Catalog />} />
<Route path="/about" element={<About />} />
<Route path="/film" element={<Film />} />
<Route path="movies" element={<Movies />} />
</Routes>
</div>
);
};

18
src/components/Footer.jsx Normal file
View File

@@ -0,0 +1,18 @@
export default function Footer() {
return (
<footer className="container mt-auto">
<div className="text-center">
<p>
@ООО ОАО ИП и так далее. Любые лицензии по номеру 78375535378. 4К йоууу
<i className="bi bi-heart ms-2"></i>
<i className="bi bi-star-fill ms-1"></i>
<i className="bi bi-arrow-right ms-1"></i>
</p>
<div>
<i className="bi bi-telegram"> Телеграм</i>
<i className="bi bi-discord ms-1"> Дискорд</i>
</div>
</div>
</footer>
)
}

25
src/components/Header.jsx Normal file
View File

@@ -0,0 +1,25 @@
import { Link } from 'react-router-dom'
export default function Header() {
return (
<header className="container mt-4 mb-4">
<div className="row mb-2 align-items-baseline">
<img src="/images/4k.jpg" id="logo" className="col-auto" alt="Лого" style={{ width: '200px', height: 'auto' }} />
<h1 className="col display-1">Онлайн кинотеатр</h1>
</div>
<nav>
<ul className="nav">
<li className="nav-item dropdown">
<Link to="/catalog" className="nav-link dropdown-toggle" data-bs-toggle="dropdown"> Фильмы и сериалы </Link>
<ul className="dropdown-menu">
<li className="dropdown-item"><Link to="/catalog?genre=horror" className="nav-link">Ужастики</Link></li>
<li className="dropdown-item"><Link to="/catalog?genre=anime" className="nav-link">Аниме</Link></li>
</ul>
</li>
<li className="nav-item"><Link to="/" className="nav-link">Главная</Link></li>
<li className="nav-item"><Link to="/about" className="nav-link">О нас</Link></li>
<li className="nav-item"><Link to="/help" className="nav-link">Помощь</Link></li>
</ul>
</nav>
</header>
)
}

View File

@@ -0,0 +1,19 @@
export default function MovieCard({ movie, onEdit, onDelete }) {
return (
<div className="card h-100">
<img src={movie.poster} className="card-img-top" alt={movie.title} />
<div className="card-body">
<h5 className="card-title">{movie.title}</h5>
</div>
<div className="card-footer d-flex justify-content-between">
<button className="btn btn-sm btn-primary" onClick={onEdit}>
Редактировать
</button>
<button className="btn btn-sm btn-danger" onClick={onDelete}>
Удалить
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,57 @@
import { useEffect, useState } from 'react';
export default function MovieForm({ initialData = {}, onSubmit }) {
const [title, setTitle] = useState('')
const [poster, setPoster] = useState('')
useEffect(() => {
if (initialData) {
// @ts-ignore
setTitle(initialData.title || '')
// @ts-ignore
setPoster(initialData.poster || '')
}
}, [initialData])
const handleSubmit = (e) => {
e.preventDefault()
onSubmit({
...initialData,
title,
poster,
})
}
return (
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Название</label>
<input
type="text"
className="form-control"
value={title}
onChange={e => setTitle(e.target.value)}
required
/>
</div>
<div className="mb-4">
<label className="form-label">URL постера</label>
<input
type="url"
className="form-control"
value={poster}
onChange={e => setPoster(e.target.value)}
required
/>
</div>
<button type="submit" className="btn btn-primary" >
{initialData?.
// @ts-ignore
id ? 'Сохранить' : 'Добавить'}
</button>
</form>
)
}

15
src/index.jsx Normal file
View File

@@ -0,0 +1,15 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { App } from "./app/App";
import 'bootstrap-icons/font/bootstrap-icons.css';
import 'bootstrap/dist/css/bootstrap.min.css';
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

25
src/pages/About.jsx Normal file
View File

@@ -0,0 +1,25 @@
import Footer from '../components/Footer'
import Header from '../components/Header'
export default function About() {
return (
<>
<Header />
<div className="container">
<h2 className="mb-3">О нас</h2>
<p className="mb-0">
Мы рады приветствовать вас на данном сайте.
</p>
<p>
Если есть предложения для улучшения работы сайта, пишите на почту&nbsp;
<a href="mailto:ilyasvaliylov@gmail.com">
ilyasvaliylov@gmail.com
</a>
</p>
</div>
<Footer />
</>
)
}

28
src/pages/Catalog.jsx Normal file
View File

@@ -0,0 +1,28 @@
import Footer from '../components/Footer'
import Header from '../components/Header'
export default function Catalog() {
return (
<>
<Header />
<div className="container mb-4">
<h2>Каталог фильмов и сериалов</h2>
<div className="d-md-flex flex-wrap justify-content-between">
{Array.from({ length: 9 }).map((_, i) => (
<div key={i} className="w-25 me-3 mb-4 text-center">
<figure>
<figcaption className="mb-2">
<a href="/film" className="fs-5 link-dark text-decoration-none">
Ловцы забытых голосов (2011)
</a>
</figcaption>
<img src="/images/film.jpg" className="img-fluid" alt="Постер" />
</figure>
</div>
))}
</div>
</div>
<Footer />
</>
)
}

43
src/pages/Film.jsx Normal file
View File

@@ -0,0 +1,43 @@
import Footer from '../components/Footer'
import Header from '../components/Header'
export default function Film() {
return (
<>
<Header />
<div className="container">
<div className="film">
<h2 className="card-title mb-4">Ловцы забытых голосов (2011)</h2>
<p className="card-text mb-4">
Юная Асуна живет с вечно пропадающей на работе матерью и после школы
любит забираться на скалу, чтобы послушать радио, доставшееся от
умершего отца. Однажды в этом своем секретном месте она знакомится с
загадочным юношей Сюном, который говорит, что пришел из далекой страны
Агартхи. Эта встреча становится началом полного приключений
путешествия в волшебный мир, где Асуне предстоит столкнуться с
потерями и обрести надежду.
</p>
<img src="/images/film.jpg" className="img-fluid w-25 me-4 mb-4" alt="Постер фильма" />
<ul className="list-unstyled film-container mb-4">
<li><strong>Год производства:</strong> 2011</li>
<li><strong>Жанр:</strong> аниме, мультфильм, драма, приключения</li>
<li><strong>Режиссер:</strong> Макото Синкай</li>
<li><strong>Сборы в мире:</strong> $600 486</li>
</ul>
<p>
<a href="https://www.kinopoisk.ru/film/581102/" target="_blank" rel="noopener noreferrer">
Смотреть фильм на КиноПоиск
</a>
</p>
</div>
</div>
<Footer />
</>
)
}

29
src/pages/Home.jsx Normal file
View File

@@ -0,0 +1,29 @@
import Footer from '../components/Footer'
import Header from '../components/Header'
export default function Home() {
return (
<>
<Header />
<div className="container">
<h2>Добро пожаловать в онлайн кинотеатр</h2>
<p>Смотрите фильмы бесплатно и без ограничений.</p>
<h2>Премьеры недели</h2>
<div className="d-md-flex flex-wrap justify-content-between">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="w-25 me-3 mb-4">
<h4>Ловцы забытых голосов</h4>
<figure>
<img src="/images/film.jpg" className="img-fluid" alt="Постер" />
<figcaption>
<a href="/film" target="_blank" rel="noopener noreferrer">Смотреть</a>
</figcaption>
</figure>
</div>
))}
</div>
</div>
<Footer />
</>
)
}

69
src/pages/Movies.jsx Normal file
View File

@@ -0,0 +1,69 @@
import { useState } from 'react';
import { useMovies } from '../../hooks/useMovies';
import Footer from '../components/Footer';
import Header from '../components/Header';
import MovieCard from '../components/MovieCard';
import MovieForm from '../components/MovieForm';
export default function Movies() {
const { movies, addMovie, updateMovie, deleteMovie } = useMovies();
const [showForm, setShowForm] = useState(false);
const [editingMovie, setEditingMovie] = useState(null);
const handleAdd = () => {
setEditingMovie(null);
setShowForm(true);
};
const handleEdit = (movie) => {
setEditingMovie(movie);
setShowForm(true);
};
const handleDelete = async (id) => {
await deleteMovie(id);
};
const handleSubmit = async (movieData) => {
const success = editingMovie
? await updateMovie(movieData)
: await addMovie(movieData);
if (success) {
setShowForm(false);
}
};
return (
<>
<Header />
<div className="container mt-4">
<h1 className="mb-4">Мои фильмы</h1>
<button className="btn btn-success mb-4" onClick={handleAdd}>
Добавить новый фильм
</button>
{showForm && (
<MovieForm
initialData={editingMovie}
onCancel={() => setShowForm(false)}
onSubmit={handleSubmit}
/>
)}
<div className="row">
{movies.map(movie => (
<div key={movie.id} className="col-md-4 mb-4">
<MovieCard
movie={movie}
onEdit={() => handleEdit(movie)}
onDelete={() => handleDelete(movie.id)}
/>
</div>
))}
</div>
</div>
<Footer />
</>
);
}

6
vite.config.js Normal file
View File

@@ -0,0 +1,6 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
});