7 Commits

Author SHA1 Message Date
maxim
c80d1be3c5 отчет 2025-05-28 22:02:17 +04:00
maxim
e5f5540fe8 отчет 2025-05-28 21:53:35 +04:00
maxim
49c622ff69 лаба 2025-05-28 21:51:38 +04:00
maxim
499b51d2e1 лаба + отчет 2025-05-28 20:58:31 +04:00
maxim
51875bcc5e лаба + отчет 2025-05-28 20:22:51 +04:00
maxim
51545137be лаба + отчет 2025-05-28 19:45:47 +04:00
maxim
0dd5c03d7d вторая лаба + отчет 2025-05-28 19:30:55 +04:00
62 changed files with 19359 additions and 316 deletions

14
.gitignore vendored
View File

@@ -1,14 +0,0 @@
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix

Binary file not shown.

BIN
LabWork6Report.docx Normal file

Binary file not shown.

View File

@@ -1,2 +0,0 @@
# InternetDev

View File

@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<link rel="stylesheet" href="styles.css">
<head>
<title>Каталог</title>
</head>
<body>
<h3>Тут разбита музыка на жанры</h3>
<p>Инфа будет +- такая</p>
<div class="list">
<ul class="punk-list">
<li>
<div class="item">
<img class="catalog" src="pankrock.jpg" alt="Панк-Рок" width=200>
<a href="punkrock.html">Панк-Рок</a>
</div>
</li>
<li>
<div class="item">
<img class="catalog" src="psy.png" alt="Психоделический рок" width=200>
<a href="">Психоделика</a>
</div>
</li>
<li>
<div class="item">
<img class="catalog" src="garajnipunk.jpg" alt="Гаражный-панк" width=200>
<a href="">Гражный-панк</a>
</div>
</li>
</ul>
</div>
<a href="index.html"> Вернуться назад</a>
</body>
</html>

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<link rel="stylesheet" href="styles.css">
<head>
<title>Гражданская оборона</title>
</head>
<body>
<p>Здесь можно почитать инфу про исполнителя и перейти на песню</p>
<img src = "grob.jpg" alt="Гражданская оборона" width= 500>
<div class="descriptionForSong">
<p>
«Гражданская Оборона» — культовая советская и российская рок-группа, основанная в 1984 году в Омске Егором Летовым.
Коллектив стал одним из самых влиятельных в андеграундной среде, а его творчество оказало огромное влияние
на развитие русского рока. Группа известна своими резкими, часто провокационными текстами, которые затрагивали
темы социального протеста, экзистенциальных переживаний и критики общества.
</p>
<p>
Музыка «Гражданской Обороны» сочетает в себе элементы панк-рока, гаражного рока и лоу-фая.
Несмотря на минималистичный подход к звучанию, группа смогла создать уникальный стиль, который
стал узнаваемым и вдохновил множество последующих исполнителей.
</p>
<p>
Среди самых известных альбомов группы — «Тоталитаризм», «Мышеловка», «Здорово и вечно»,
«Русское поле экспериментов» и «Инструкция по выживанию». Творчество «Гражданской Обороны»
остается актуальным и по сей день, а Егор Летов считается одной из ключевых фигур в истории
русской рок-музыки.
</p>
</div>
<p>
Песни:
<ul>
<li><a href = "grobKaifIliBolshe.html">Кайф или больше</a></li>
<li><a href = "">Зоопарк</a></li>
<li><a href = "">Новая патриотическая</a></li>
</ul>
</p>
<a href="index.html"> Вернуться назад</a>
</body>
</html>

View File

@@ -1,49 +0,0 @@
//grobKaifIliBolshe
<!DOCTYPE html>
<html lang="ru">
<link rel="stylesheet" href="styles.css">
<head>
<title>Гражданская оборона - "Кайф или больше"</title>
</head>
<body>
<h3>Тут будет типа песня</h3>
<img src = "nekrofilia.jpg" alt = "Обложка" width="500" >
<h2>Кайф или больше</h2>
<a href="grob.html">Гражданская оборона</a>
<p>Описание</p>
<ul>
<li>Была выпущена в 1987 году</li>
<li>Входит в альбом некрофилия</li>
</ul>
<div class = "text">
<p>Текст песни:
<br> [Куплет 1]
<br>Рука повисла в небе, полном до краёв
<br>Мои ошибки устилают мой позор
<br>Я сочно благодарен, словно кошкин блёв
<br>И смачно богомолен, словно приговор
<br>[Припев]
<br>Но мне придётся выбирать
<br>Кайф или больше
<br>Рай или больше
<br>Свет или больше… Хей-йо
<br>[Куплет 2]
<br>Я буду ласковым, как тёплый банный лист
<br>Я буду вежливым, как битое окно
<br>Я буду благотворен, словно онанист
<br>Я буду зазеркален, словно всё равно
<br>[Припев]
<br>Но мне придётся выбирать
<br>Кайф или больше
<br>Рай или больше
<br>Свет или больше...хей-йо
</p>
</div>
</body>
<a href="index.html"> Вернуться назад</a>
</html>

View File

@@ -1,28 +0,0 @@
//index
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<title>Панкуха</title>
</head>
<body>
<div class="content">
<h1>Стриминговый сервис <em>"Панкуха"</em></h1>
<img src="res/logo.png" alt="Эмблема" width="200">
<div class="main">
<div class="description">
<p><span style="background-color: blueviolet;">Какие страницы я реализовал:</span></p>
</div>
<ol class = "spisok">
<a href="index.html"><li class="spisok_el">Главная страница</li></a>
<a href="grob.html"><li>Страница исполнителя</li></a>
<li><a href="grobKaifIliBolshe.html">Страница песни</a></li>
<a href="catalog.html"><li>Каталог</li></a>
<a href="punkrock.html"><li>Страница каталога</li></a>
</ol>
</div>
</div>
</body>
</html>

23
punk-rock-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

46
punk-rock-app/README.md Normal file
View File

@@ -0,0 +1,46 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

114
punk-rock-app/db.json Normal file
View File

@@ -0,0 +1,114 @@
{
"artists": [
{
"id": "6",
"name": "The Clash",
"description": "Pioneers of British punk rock, known for their political lyrics.",
"epochId": "1",
"countryId": "1"
},
{
"id": "7",
"name": "Sex Pistols",
"description": "Notorious for their rebellious attitude and controversial lyrics.",
"epochId": "1",
"countryId": "1"
},
{
"id": "8",
"name": "The Ramones",
"description": "Influential American punk rock band, known for their fast-paced music.",
"epochId": "1",
"countryId": "1"
},
{
"id": "9",
"name": "The Damned",
"description": "First British punk band to release a single and an album.",
"epochId": "1",
"countryId": "1"
},
{
"id": "11",
"name": "Red Hot Chili Peppers",
"description": "Known for their unique fusion of punk rock and funkf",
"epochId": "2",
"countryId": "2",
"epoch": {
"id": "2",
"name": "1990s"
},
"country": {
"id": "2",
"name": "UK"
}
},
{
"id": "7fd4c5fb-933a-4496-967a-8d6b92252603",
"name": "ыфвфвы",
"description": "аыввавы",
"epochId": "2",
"countryId": "2"
},
{
"id": "ac9dfa68-fd2c-44d4-8c25-fd9b417c2821",
"name": "fdfddf",
"description": "efeffdas",
"epochId": "2",
"countryId": "1"
},
{
"id": "b8a25883-51c4-4075-87d7-a1906a34962e",
"name": "Green Day",
"description": "дддд",
"epochId": "1",
"countryId": "1"
}
],
"epochs": [
{
"id": "1",
"name": "1970s"
},
{
"id": "2",
"name": "1990s"
}
],
"countries": [
{
"id": "1",
"name": "USA"
},
{
"id": "2",
"name": "UK"
}
],
"subscriptions": [
{
"id": "2",
"userId": "2",
"type": "Premium",
"price": 15,
"active": true
},
{
"id": "4d38",
"type": "Premium",
"price": 15,
"userId": 1,
"active": true
}
],
"users": [
{
"id": "1",
"name": "User1"
},
{
"id": "2",
"name": "User2"
}
]
}

17731
punk-rock-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
{
"name": "punk-rock-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.126",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5",
"@types/react-router-dom": "^5.3.3",
"axios": "^1.9.0",
"bootstrap": "^5.3.6",
"bootstrap-icons": "^1.13.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"uuid": "^11.1.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/styles.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<title>Punk Rock App</title>
</head>
<body class="bg-dark text-light">
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View File

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 240 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 969 KiB

After

Width:  |  Height:  |  Size: 969 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,191 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--punk-primary: blueviolet;
--punk-dark: #121212;
}
a {
font-size: 16px;
font-weight: 500;
color: blueviolet;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background-color: var(--punk-dark);
color: white;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
text-align: center;
}
.lyrics {
text-align: center;
line-height: 1.8;
font-size: 1.1rem;
}
.chorus {
font-weight: bold;
margin: 1.5rem 0;
}
#app {
width: 100%;
margin: 0;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #f7df1eaa);
}
/* Анимация карточек */
.catalog-item {
transition: transform 0.3s;
}
.catalog-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(138, 43, 226, 0.3);
}
.btn-punk {
background-color: blueviolet !important; /* Увеличиваем специфичность */
color: white !important;
border: none !important;
padding: 0.6em 1.2em; /* Убедимся, что есть отступы */
display: inline-block; /* Убедимся, что кнопка видна */
visibility: visible; /* Убедимся, что не скрыта */
opacity: 1; /* Убедимся, что не прозрачна */
}
.btn-punk:hover {
background-color: #9d4edd !important;
color: white !important;
}
.artist-card {
transition: all 0.3s;
}
.artist-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(138, 43, 226, 0.4);
}
.bg-punk {
background-color: blueviolet !important;
}
.btn-outline-punk {
color: blueviolet;
border-color: blueviolet;
}
.btn-outline-punk:hover {
background-color: blueviolet;
color: white;
}
.card {
padding: 2em;
color: blueviolet;
}
.read-the-docs {
color: #888;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.bg-punk {
background-color: var(--punk-primary) !important;
}
.text-punk {
color: var(--punk-primary) !important;
}
.border-punk {
border-color: var(--punk-primary) !important;
}
.nav-link:hover, .dropdown-item:hover {
color: white !important;
background-color: var(--punk-primary) !important;
}
.list-group-item-action:hover {
transform: translateX(5px);
transition: transform 0.3s;
}
.lead{
color: blueviolet;
text-align: center;
font-size: 20pt;
}
.navbar {
box-shadow: 0 0 15px rgba(138, 43, 226, 0.4);
}
.dropdown-menu {
background-color: #000 !important;
}
.nav-link:hover,
.nav-link:focus {
text-shadow: 0 0 8px blueviolet;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

38
punk-rock-app/src/App.css Normal file
View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

53
punk-rock-app/src/App.tsx Normal file
View File

@@ -0,0 +1,53 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import Home from './pages/Home';
import Catalog from './pages/Catalog';
import ArtistPage from './pages/ArtistPage';
import ArtistsPage from './pages/ArtistsPage';
const App: React.FC = () => {
return (
<Router>
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
<div className="container">
<Link className="navbar-brand text-punk" to="/">Панкуха</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">
<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="/artist">Исполнитель</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/artists">Список исполнителей</Link>
</li>
</ul>
</div>
</div>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/catalog" element={<Catalog />} />
<Route path="/artist" element={<ArtistPage />} />
<Route path="/artists" element={<ArtistsPage />} />
</Routes>
</Router>
);
};
export default App;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Artist, Epoch, Country } from '../types';
interface ArtistCardProps {
artist: Artist;
epochs: Epoch[];
countries: Country[];
onEdit: (artist: Artist) => void;
onDelete: (id: string) => void;
}
const ArtistCard: React.FC<ArtistCardProps> = ({ artist, epochs, countries, onEdit, onDelete }) => {
// Получаем имена эпохи и страны по идентификаторам
const epochName = epochs.find(e => e.id === artist.epochId)?.name || 'Не указана';
const countryName = countries.find(c => c.id === artist.countryId)?.name || 'Не указана';
return (
<div className="col-md-4 mb-4">
<div className="card bg-dark border-punk">
<div className="card-body">
<h5 className="card-title text-punk">{artist.name}</h5>
<p className="card-text text-light">{artist.description || 'Нет описания'}</p>
<p className="card-text text-light"><small>Эпоха: {epochName}</small></p>
<p className="card-text text-light"><small>Страна: {countryName}</small></p>
<button
className="btn btn-outline-primary edit-btn me-2"
onClick={() => onEdit(artist)}
>
<i className="bi bi-pencil-square"></i> Изменить
</button>
<button
className="btn btn-outline-danger delete-btn"
onClick={() => onDelete(artist.id)}
>
<i className="bi bi-trash"></i> Удалить
</button>
</div>
</div>
</div>
);
};
export default ArtistCard;

View File

@@ -0,0 +1,161 @@
import React, { useState, useEffect, ChangeEvent } from 'react';
import { Artist, Epoch, Country } from '../types';
interface ArtistFormProps {
countries: Country[];
epochs: Epoch[];
onSubmit: (artist: Artist) => Promise<void>;
artist?: Artist | null;
onCancel?: () => void;
}
const ArtistForm: React.FC<ArtistFormProps> = ({ countries, epochs, onSubmit, artist, onCancel }) => {
const [formData, setFormData] = useState<Artist>({
id: '',
name: '',
description: '',
epochId: '',
countryId: '',
});
const [error, setError] = useState<string | null>(null);
useEffect(() => {
console.log('ArtistForm artist prop:', artist);
if (artist) {
setFormData({
id: artist.id,
name: artist.name,
description: artist.description,
epochId: artist.epochId,
countryId: artist.countryId,
});
} else {
setFormData({
id: '',
name: '',
description: '',
epochId: '',
countryId: '',
});
}
}, [artist]);
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name || !formData.description || !formData.epochId || !formData.countryId) {
setError('Все поля обязательны!');
return;
}
try {
setError(null);
if (artist) {
// Если редактируем существующего артиста, передаем id
await onSubmit({ ...formData, id: artist.id });
} else {
// Если добавляем нового артиста, id не передаем
await onSubmit(formData);
}
setFormData({
id: '',
name: '',
description: '',
epochId: '',
countryId: '',
});
} catch (err: any) {
setError(err.message || 'Ошибка при добавлении/редактировании исполнителя');
}
};
console.log('Rendering form with formData:', formData);
return (
<div className="card bg-dark border-punk">
<div className="card-body">
<h3 className="text-punk mb-4">
<i className={`bi ${artist ? 'bi-pencil-square' : 'bi-person-plus'}`}></i>
{artist ? 'Редактировать исполнителя' : 'Добавить исполнителя'}
</h3>
{error && <div className="alert alert-danger">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label text-light">Название группы</label>
<input
type="text"
className="form-control bg-dark text-light border-punk"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="mb-3">
<label className="form-label text-light">Описание</label>
<textarea
className="form-control bg-dark text-light border-punk"
name="description"
value={formData.description}
onChange={handleChange}
required
/>
</div>
<div className="mb-3">
<label className="form-label text-light">Эпоха</label>
<select
className="form-select bg-dark text-light border-punk"
name="epochId"
value={formData.epochId}
onChange={handleChange}
required
>
<option value={0}>Выберите эпоху</option>
{epochs.map(epoch => (
<option key={epoch.id} value={epoch.id}>{epoch.name}</option>
))}
</select>
</div>
<div className="mb-3">
<label className="form-label text-light">Страна</label>
<select
className="form-select bg-dark text-light border-punk"
name="countryId"
value={formData.countryId}
onChange={handleChange}
required
>
<option value={0}>Выберите страну</option>
{countries.map(country => (
<option key={country.id} value={country.id}>{country.name}</option>
))}
</select>
</div>
<button
type="submit"
className="btn btn-punk mt-3"
style={{ zIndex: 1000 }}
>
<i className={`bi ${artist ? 'bi-save' : 'bi-plus-circle'}`}></i>
{artist ? 'Сохранить изменения' : 'Добавить исполнителя'}
</button>
{artist && <button type="button" className="btn btn-secondary mt-3 ms-2" onClick={onCancel}>Отмена</button>}
</form>
</div>
</div>
);
};
export default ArtistForm;

View File

@@ -0,0 +1,30 @@
import React from 'react';
import ArtistCard from './ArtistCard';
import { Artist, Epoch, Country } from '../types';
interface ArtistListProps {
artists: Artist[];
epochs: Epoch[];
countries: Country[];
onEdit: (artist: Artist) => void;
onDelete: (id: string) => void;
}
const ArtistList: React.FC<ArtistListProps> = ({ artists, epochs, countries, onEdit, onDelete }) => {
return (
<div className="row row-cols-1 row-cols-md-3 g-4 mt-4">
{artists.map(artist => (
<ArtistCard
key={artist.id}
artist={artist}
epochs={epochs}
countries={countries}
onEdit={onEdit}
onDelete={onDelete}
/>
))}
</div>
);
};
export default ArtistList;

View File

@@ -0,0 +1,18 @@
import React from 'react';
const Footer: React.FC = () => {
return (
<footer className="bg-black py-3 border-top border-punk mt-auto">
<div className="container">
<div className="d-flex flex-wrap justify-content-between align-items-center">
<p className="mb-0 text-punk">© 2025. Все права защищены.</p>
<nav className="d-flex align-items-center">
<a href="#" className="text-punk me-3">Политика конфиденциальности</a>
</nav>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { Link } from 'react-router-dom';
const Header: React.FC = () => {
return (
<header className="sticky-top navbar navbar-expand-lg navbar-dark bg-black border-bottom border-punk px-0">
<div className="container-fluid">
<Link to="/" className="navbar-brand d-flex align-items-center ms-3">
<span className="text-punk fs-4 fw-bold">Панкуха</span>
</Link>
<button className="navbar-toggler me-3" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent">
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse bg-black" id="navbarContent">
<ul className="navbar-nav w-100 justify-content-end pe-4">
<li className="nav-item">
<Link className="nav-link text-punk fw-bold" to="/contacts">
<i className="bi bi-people-fill me-1"></i>Контакты
</Link>
</li>
</ul>
</div>
</div>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,40 @@
import React, { useState, ChangeEvent } from 'react';
import { Subscription } from '../types';
interface SubscriptionFormProps {
onSubmit: (subscription: Omit<Subscription, 'id' | 'active' | 'userId'>) => Promise<void>;
}
const SubscriptionForm: React.FC<SubscriptionFormProps> = ({ onSubmit }) => {
const [subscriptionType, setSubscriptionType] = useState<string>('Basic');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await onSubmit({ type: subscriptionType, price: subscriptionType === 'Basic' ? 5 : 15 });
};
const handleChange = (e: ChangeEvent<HTMLSelectElement>) => {
setSubscriptionType(e.target.value);
};
return (
<div className="card bg-dark border-punk m-2">
<div className="card-body">
<h5 className="text-punk">Оформить подписку</h5>
<form onSubmit={handleSubmit}>
<select
className="form-select bg-dark text-light border-punk mb-3"
value={subscriptionType}
onChange={handleChange}
>
<option value="Basic">Базовая ($5)</option>
<option value="Premium">Премиум ($15)</option>
</select>
<button type="submit" className="btn btn-punk">Подписаться</button>
</form>
</div>
</div>
);
};
export default SubscriptionForm;

View File

@@ -0,0 +1,138 @@
import { useState, useEffect } from 'react';
import { getArtists, createArtist, updateArtist, deleteArtist, getEpochs, getCountries } from '../services/api';
import { Artist, Epoch, Country } from '../types';
import { v4 as uuidv4 } from 'uuid';
const useArtists = () => {
const [artists, setArtists] = useState<Artist[]>([]);
const [epochs, setEpochs] = useState<Epoch[]>([]);
const [countries, setCountries] = useState<Country[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const fetchData = async () => {
try {
setLoading(true);
const [artistsData, epochsData, countriesData] = await Promise.all([
getArtists(),
getEpochs(),
getCountries(),
]);
console.log('Artists data:', artistsData.data);
console.log('Epochs data:', epochsData.data);
console.log('Countries data:', countriesData.data);
const enrichedArtists = artistsData.data.map((artist: Artist) => ({
...artist,
epoch: epochsData.data.find((e: Epoch) => e.id.toString() === artist.epochId.toString()),
country: countriesData.data.find((c: Country) => c.id.toString() === artist.countryId.toString()),
}));
const sortedArtists = [...enrichedArtists].sort((a, b) =>
sortOrder === 'asc'
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name)
);
setArtists(sortedArtists);
setEpochs(epochsData.data);
setCountries(countriesData.data);
setLoading(false);
} catch (err: any) {
setError(err.message);
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [sortOrder]);
const addArtist = async (artist: Omit<Artist, 'id'>) => {
const nameExists = artists.some(a => a.name.toLowerCase() === artist.name.toLowerCase());
if (nameExists) {
throw new Error('Исполнитель с таким именем уже существует!');
}
// Генерируем уникальный id на клиентской стороне
const uniqueId = uuidv4();
// Создаем новый объект артиста с id
const artistWithId: Artist = {
...artist,
id: uniqueId,
epochId: artist.epochId.toString(),
countryId: artist.countryId.toString(),
};
// Отправляем объект на сервер
const response = await createArtist(artistWithId);
const newArtist = {
...response.data,
epoch: epochs.find(e => e.id.toString() === response.data.epochId.toString()),
country: countries.find(c => c.id.toString() === response.data.countryId.toString()),
};
const updatedArtists = [...artists, newArtist].sort((a, b) =>
sortOrder === 'asc'
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name)
);
setArtists(updatedArtists);
};
const editArtist = async (id: string, artistData: Partial<Artist>) => {
if (artistData.name !== undefined) {
const nameExists = artists.some(a => a.id !== id && a.name.toLowerCase() === artistData.name!.toLowerCase());
if (nameExists) {
throw new Error('Исполнитель с таким именем уже существует!');
}
}
try {
const response = await updateArtist(id, artistData);
const updatedArtist = {
...response.data,
epoch: epochs.find(e => e.id.toString() === response.data.epochId.toString()),
country: countries.find(c => c.id.toString() === response.data.countryId.toString()),
};
const updatedArtists = artists.map(a => a.id === id ? updatedArtist : a);
setArtists(updatedArtists.sort(((a, b) =>
sortOrder === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
)));
} catch (err: any) {
setError(`Ошибка при изменении: ${err.message}. Сервер ответил ${err.response?.status}`);
console.error('Edit error:', err);
}
};
const removeArtist = async (id: string) => {
console.log(`Attempting to delete artist with id: ${id}`);
console.log('Current artists:', artists);
const artistExists = artists.find(a => a.id.toString() === id.toString());
if (!artistExists) {
setError(`Ошибка: Исполнитель с id ${id} не найден в текущем списке!`);
return;
}
try {
await deleteArtist(id);
// Повторно загружаем данные с сервера после удаления
await fetchData();
} catch (err: any) {
setError(`Ошибка при удалении: ${err.message}. Сервер ответил ${err.response?.status}`);
console.error('Delete error:', err);
}
};
const toggleSortOrder = () => {
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
};
return { artists, epochs, countries, loading, error, addArtist, editArtist, removeArtist, toggleSortOrder, sortOrder };
};
export default useArtists;

View File

@@ -0,0 +1,46 @@
import { useState, useEffect } from 'react';
import { getSubscriptions, createSubscription, deleteSubscription, getUsers } from '../services/api';
import { Subscription, User } from '../types';
const useSubscriptions = (userId: number) => {
const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const [subscriptionsData, usersData] = await Promise.all([
getSubscriptions(),
getUsers(),
]);
setSubscriptions(subscriptionsData.data);
setUsers(usersData.data);
setLoading(false);
} catch (err: any) {
setError(err.message);
setLoading(false);
}
};
fetchData();
}, []);
const addSubscription = async (subscription: Omit<Subscription, 'id' | 'active' | 'userId'>) => {
const response = await createSubscription({ ...subscription, userId, active: true });
setSubscriptions([...subscriptions, response.data]);
};
const cancelSubscription = async (id: number) => {
await deleteSubscription(id);
setSubscriptions(subscriptions.filter(s => s.id !== id));
};
const getUserSubscription = (): Subscription | undefined => {
return subscriptions.find(s => s.userId === userId);
};
return { subscriptions, users, loading, error, addSubscription, cancelSubscription, getUserSubscription };
};
export default useSubscriptions;

View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

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

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,77 @@
import React from 'react';
const ArtistPage: React.FC = () => {
return (
<div className="container my-5 flex-grow-1">
<div className="row">
<div className="col-lg-8 mx-auto">
<h1 className="display-4 text-punk mb-4">
<i className="bi bi-person-badge"></i> Гражданская Оборона
</h1>
{/* Изображение (путь /res/grob.jpg из public) */}
<div className="text-center mb-5">
<img
src="/res/grob.jpg"
alt="Гражданская Оборона"
className="img-fluid rounded border border-punk"
style={{ maxWidth: '100%', height: 'auto' }}
/>
</div>
<p className="lead text-punk">
Здесь можно почитать инфу про исполнителя и перейти на песню
</p>
<div className="card bg-dark border-punk mb-4">
<div className="card-body">
<div className="descriptionForSong">
<p className="text-light">
«Гражданская Оборона» культовая советская и российская рок-группа, основанная в 1984 году в Омске Егором Летовым.
Коллектив стал одним из самых влиятельных в андеграундной среде.
</p>
<p className="text-light">
Музыка «Гражданской Обороны» сочетает в себе элементы панк-рока, гаражного рока и лоу-фая.
Несмотря на минималистичный подход к звучанию, группа смогла создать уникальный стиль.
</p>
<p className="text-light">
Среди самых известных альбомов группы «Тоталитаризм», «Мышеловка», «Здорово и вечно»,
«Русское поле экспериментов» и «Инструкция по выживанию». Творчество «Гражданской Обороны»
остается актуальным и по сей день, а Егор Летов считается одной из ключевых фигур в истории
русской рок-музыки.
</p>
</div>
</div>
</div>
<div className="card bg-dark border-punk">
<div className="card-body">
<h3 className="text-punk mb-3">
<i className="bi bi-music-note-list"></i> Популярные песни:
</h3>
<ul className="list-group list-group-flush">
<li className="list-group-item bg-dark text-punk border-punk">
<a href="#" className="text-punk text-decoration-none">
<i className="bi bi-file-music me-2"></i> Кайф или больше
</a>
</li>
<li className="list-group-item bg-dark text-punk border-punk">
<a href="#" className="text-punk text-decoration-none">
<i className="bi bi-file-music me-2"></i> Зоопарк
</a>
</li>
<li className="list-group-item bg-dark text-punk border-punk">
<a href="#" className="text-punk text-decoration-none">
<i className="bi bi-file-music me-2"></i> Новая патриотическая
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
);
};
export default ArtistPage;

View File

@@ -0,0 +1,168 @@
import React, { useState } from 'react';
import useArtists from '../hooks/useArtists';
import ArtistList from '../components/ArtistList';
import { Artist, Epoch, Country } from '../types';
const ArtistsPage: React.FC = () => {
const { artists, loading, error, removeArtist, editArtist, epochs, countries, sortOrder } = useArtists();
const [editingArtist, setEditingArtist] = useState<Artist | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [sortConfig, setSortConfig] = useState<{ field: 'name' | 'epoch' | 'country'; direction: 'asc' | 'desc' }>({
field: 'name',
direction: 'asc'
});
// Константы для пагинации
const artistsPerPage = 6;
const indexOfLastArtist = currentPage * artistsPerPage;
const indexOfFirstArtist = indexOfLastArtist - artistsPerPage;
const sortedArtists = [...artists].sort((a, b) => {
const getFieldValue = (artist: Artist) => {
if (sortConfig.field === 'name') return artist.name;
if (sortConfig.field === 'epoch') return artist.epoch?.name || '';
return artist.country?.name || '';
};
const valueA = getFieldValue(a);
const valueB = getFieldValue(b);
if (valueA < valueB) return sortConfig.direction === 'asc' ? -1 : 1;
if (valueA > valueB) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
// Текущие артисты для отображения
const currentArtists = sortedArtists.slice(indexOfFirstArtist, indexOfLastArtist);
if (loading) return <p className="text-center text-punk my-5">Загрузка...</p>;
if (error) return <p className="text-center text-danger my-5">Ошибка: {error}</p>;
const handleEdit = (artist: Artist) => {
setEditingArtist(artist);
};
const handleDelete = (id: string) => {
removeArtist(id);
// Сброс страницы при удалении
if (currentArtists.length === 1 && currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
const handleSave = async () => {
if (editingArtist) {
await editArtist(editingArtist.id, editingArtist);
setEditingArtist(null);
}
};
const toggleSort = (field: 'name' | 'epoch' | 'country') => {
setSortConfig(prev => ({
field,
direction: prev.field === field && prev.direction === 'asc' ? 'desc' : 'asc'
}));
};
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
return (
<div className="container py-4">
<h1 className="text-punk mb-3">Список исполнителей</h1>
{/* Кнопки сортировки */}
<div className="d-flex gap-2 mb-3">
<button
className={`btn btn-outline-punk text-light ${sortConfig.field === 'name' ? 'active' : ''}`}
onClick={() => toggleSort('name')}
>
По имени {sortConfig.field === 'name' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
</button>
<button
className={`btn btn-outline-punk text-light ${sortConfig.field === 'epoch' ? 'active' : ''}`}
onClick={() => toggleSort('epoch')}
>
По эпохе {sortConfig.field === 'epoch' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
</button>
<button
className={`btn btn-outline-punk text-light ${sortConfig.field === 'country' ? 'active' : ''}`}
onClick={() => toggleSort('country')}
>
По стране {sortConfig.field === 'country' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
</button>
</div>
<ArtistList artists={currentArtists} onEdit={handleEdit} onDelete={handleDelete} epochs={epochs} countries={countries} />
{/* Пагинация */}
<nav className="mt-4">
<ul className="pagination justify-content-center">
{Array.from({ length: Math.ceil(artists.length / artistsPerPage) }).map((_, index) => (
<li key={index} className={`page-item ${currentPage === index + 1 ? 'active' : ''}`}>
<button
className="page-link bg-dark text-punk border-punk"
onClick={() => paginate(index + 1)}
>
{index + 1}
</button>
</li>
))}
</ul>
</nav>
{/* Модальное окно редактирования */}
{editingArtist && (
<div className="modal show" style={{ display: 'block', backgroundColor: 'rgba(0,0,0,0.7)' }}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content bg-dark border-punk">
<div className="modal-header">
<h5 className="modal-title text-punk">Редактировать исполнителя</h5>
<button
type="button"
className="btn-close btn-close-white"
onClick={() => setEditingArtist(null)}
></button>
</div>
<div className="modal-body">
<div className="mb-3">
<label className="form-label text-light">Имя</label>
<input
type="text"
className="form-control bg-dark text-light border-punk"
value={editingArtist.name}
onChange={(e) => setEditingArtist({...editingArtist, name: e.target.value})}
/>
</div>
<div className="mb-3">
<label className="form-label text-light">Описание</label>
<textarea
className="form-control bg-dark text-light border-punk"
value={editingArtist.description}
onChange={(e) => setEditingArtist({...editingArtist, description: e.target.value})}
/>
</div>
</div>
<div className="modal-footer">
<button
className="btn btn-secondary"
onClick={() => setEditingArtist(null)}
>
Отмена
</button>
<button
className="btn btn-punk"
onClick={handleSave}
>
Сохранить
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default ArtistsPage;

View File

@@ -0,0 +1,107 @@
import React, { useState } from 'react';
import useArtists from '../hooks/useArtists';
import useSubscriptions from '../hooks/useSubscriptions';
import SubscriptionForm from '../components/SubscriptionForm';
import { Subscription, Artist } from '../types';
const Catalog: React.FC = () => {
const { artists, loading: artistsLoading, error: artistsError } = useArtists();
const {
getUserSubscription,
addSubscription,
cancelSubscription,
loading: subsLoading,
error: subsError
} = useSubscriptions(1); // Пример userId = 1
const subscription: Subscription | undefined = getUserSubscription();
const [subscriptionStartDate, setSubscriptionStartDate] = useState<Date | null>(null);
if (artistsLoading || subsLoading) return <p className="text-center text-punk my-5">Загрузка...</p>;
if (artistsError || subsError) return <p className="text-center text-danger my-5">Ошибка: {artistsError || subsError}</p>;
const handleAddSubscription = async (subscriptionData: Omit<Subscription, 'id' | 'active' | 'userId'>) => {
const response = await addSubscription(subscriptionData);
setSubscriptionStartDate(new Date()); // Установите текущую дату как дату начала подписки
};
const handleCancelSubscription = async () => {
if (subscription) {
await cancelSubscription(subscription.id);
setSubscriptionStartDate(null); // Сбросить дату начала подписки
}
};
const calculateEndDate = (startDate: Date): Date => {
const endDate = new Date(startDate);
endDate.setMonth(endDate.getMonth() + 1);
return endDate;
};
return (
<div className="container py-4">
<h1 className="text-punk mb-3">Каталог</h1>
{subscription ? (
<div className="alert bg-dark border-punk">
<h5>
{subscription.type === 'Premium' ? '💎 Премиум' : '🔹 Базовая'} подписка (${subscription.price})
</h5>
{subscriptionStartDate && (
<p className="text-light">
Подписка действует до: {calculateEndDate(subscriptionStartDate).toLocaleDateString()}
</p>
)}
<button
className="btn btn-outline-danger btn-sm mt-2"
onClick={handleCancelSubscription}
>
Отменить подписку
</button>
</div>
) : (
<div className="alert bg-dark border-punk">
<h5>У вас нет подписки</h5>
<p>Оформите подписку для доступа к каталогу!</p>
</div>
)}
{/* Форма подписки (если нет активной) */}
{!subscription && <SubscriptionForm onSubmit={handleAddSubscription} />}
{/* Контент в зависимости от подписки */}
{subscription?.type === 'Premium' ? (
<div>
<h2 className="text-punk mt-4">Полный каталог артистов</h2>
{artists.map((artist) => (
<div key={artist.id} className="card bg-dark border-punk my-2">
<div className="card-body">
<h5 className="card-title text-punk">{artist.name}</h5>
<p className="card-text text-light">{artist.description}</p>
<small className="text-light">Эпоха: {artist.epoch?.name || 'Не указана'}</small>
</div>
</div>
))}
</div>
) : subscription?.type === 'Basic' ? (
<div>
<h2 className="text-punk mt-4">Базовый каталог</h2>
<p className="text-light mb-3">Доступны только названия групп. Для полного доступа оформите Premium.</p>
{artists.map((artist) => (
<div key={artist.id} className="card bg-dark border-punk my-2">
<div className="card-body">
<h5 className="card-title text-punk">{artist.name}</h5>
</div>
</div>
))}
</div>
) : (
<div className="alert bg-dark border-punk mt-4">
<p>🔒 Чтобы увидеть каталог, оформите подписку.</p>
</div>
)}
</div>
);
};
export default Catalog;

View File

@@ -0,0 +1,12 @@
import React from 'react';
const Contacts: React.FC = () => {
return (
<div className="container py-4">
<h1 className="text-punk mb-3">Контакты</h1>
<p className="text-light">Свяжитесь с нами: <a href="https://vk.com/kadyshevever" target="_blank" rel="noopener noreferrer">vk.com/kadyshevever</a></p>
</div>
);
};
export default Contacts;

View File

@@ -0,0 +1,52 @@
import React from 'react';
import useArtists from '../hooks/useArtists';
import ArtistList from '../components/ArtistList';
import ArtistForm from '../components/ArtistForm';
import { Artist, Epoch, Country } from '../types';
const Home: React.FC = () => {
const { artists, epochs, countries, loading, error, addArtist, editArtist, removeArtist, toggleSortOrder, sortOrder } = useArtists();
const [editingArtist, setEditingArtist] = React.useState<Artist | null>(null);
if (loading) return <p className="text-center text-punk my-5">Загрузка...</p>;
if (error) return <p className="text-center text-danger my-5">Ошибка: {error}</p>;
const handleSubmit = async (artist: Artist) => {
if (editingArtist && editingArtist.id) {
await editArtist(editingArtist.id, artist);
} else {
await addArtist(artist);
}
};
return (
<div className="container py-4">
<div className="d-flex justify-content-between align-items-center mb-3">
<h1 className="text-punk mb-0">Главная</h1>
<button
className="btn btn-punk"
onClick={toggleSortOrder}
>
<i className={`bi bi-sort-alpha-${sortOrder === 'asc' ? 'down' : 'up'}`}></i>
{sortOrder === 'asc' ? 'Я → А' : 'А → Я'}
</button>
</div>
<ArtistForm
countries={countries}
epochs={epochs}
onSubmit={handleSubmit}
artist={editingArtist}
onCancel={() => setEditingArtist(null)}
/>
<ArtistList
artists={artists}
epochs={epochs}
countries={countries}
onEdit={setEditingArtist}
onDelete={removeArtist}
/>
</div>
);
};
export default Home;

View File

@@ -0,0 +1,12 @@
import React from 'react';
const SongPage: React.FC = () => {
return (
<div className="container py-4">
<h1 className="text-punk mb-3">Песня</h1>
<p className="text-light">Здесь будет список песен (пока заглушка).</p>
</div>
);
};
export default SongPage;

1
punk-rock-app/src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,21 @@
import axios from 'axios';
import { Artist, Epoch, Country, Subscription, User } from '../types';
const api = axios.create({
baseURL: 'http://localhost:3001',
});
export const getArtists = () => api.get<Artist[]>('/artists');
export const createArtist = (artist: Omit<Artist, 'id'>) => api.post<Artist>('/artists', artist);
export const updateArtist = (id: string, artist: Partial<Artist>) => api.put<Artist>(`/artists/${id}`, artist);
export const deleteArtist = (id: string) => api.delete(`/artists/${id}`);
export const getEpochs = () => api.get<Epoch[]>('/epochs');
export const getCountries = () => api.get<Country[]>('/countries');
export const getSubscriptions = () => api.get<Subscription[]>('/subscriptions');
export const createSubscription = (subscription: Omit<Subscription, 'id'>) => api.post<Subscription>('/subscriptions', subscription);
export const updateSubscription = (id: number, subscription: Partial<Subscription>) => api.put<Subscription>(`/subscriptions/${id}`, subscription);
export const deleteSubscription = (id: number) => api.delete(`/subscriptions/${id}`);
export const getUsers = () => api.get<User[]>('/users');

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@@ -0,0 +1,32 @@
export interface Artist {
id: string;
name: string;
description: string;
epochId: string;
countryId: string;
epoch?: { id: string; name: string };
country?: { id: string; name: string };
}
export interface Subscription {
id: number;
userId: number;
type: string;
price: number;
active: boolean;
}
export interface User {
id: number;
name: string;
}
export interface Epoch {
id: string;
name: string;
}
export interface Country {
id: string;
name: string;
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<link rel="stylesheet" href="styles.css">
<head>
<title>Панк-рок</title>
</head>
<body>
<h3>Тут будет перечень исполнителей, принадлежащих жанру</h3>
<p>Инфа будет +- такая</p>
<ul class="autors">
<li><a href="grob.html">Гражданская Оборона</a></li>
<li><a href="">Король и Шут</a></li>
<li><a href="">Наив</a></li>
</ul>
</body>
<br><a href="index.html"> Вернуться назад</a>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 969 KiB

View File

@@ -1,121 +0,0 @@
body{
background-color: #474444;
color: white;
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
font-size: medium;
}
a{
color: aliceblue;
text-decoration: none;
font-size: 15pt;
}
a:hover{
color:lightblue;
}
img{
display: block;
margin: 0 auto;
}
.description{
font-size: 20pt;
}
.reviews{
background-color: rgb(96, 89, 89);
margin: 15pt;
font-size: 20pt;
}
.catalog{
margin: 10pt;
}
.text{
text-align: center;
}
.text li{
list-style: none;
}
.descriptionForSong{
font-size: 20pt;
background-color: beige;
color: black;
}
.punk-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
gap: 20px;
}
.punk-list li {
display: inline-block;
}
.item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.item img {
margin-bottom: 10px;
}
.list {
font-size: 17pt;
color: black;
}
.autors {
list-style: none;
}
.main{
text-align: center;
}
.main li{
list-style: none;
margin: 15pt;
border-width: 5pt;
padding-bottom: 1pt;
border-color: aliceblue;
border: 1pt solid white;
}
footer{
text-align: center;
background-color: grey;
}
.header {
display: flex;
justify-content: space-between; /* Логотип и навигация по краям */
align-items: center; /* Выравнивание по вертикали */
background-color: #333; /* Цвет фона */
padding: 10px 20px; /* Отступы внутри шапки */
color: white; /* Цвет текста */
}
.logo img {
height: 50px;
}
.nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 20px;
}
.nav a {
color: white;
text-decoration: none;
font-size: 18px;
}
.nav a:hover {
text-decoration: underline;
}

Binary file not shown.

BIN
~$bWork6Report.docx Normal file

Binary file not shown.