16 Commits
Lab2 ... Lab5

Author SHA1 Message Date
d9f91741bb еще правки для бэка 2025-12-02 13:00:31 +04:00
4d5eeb0459 обновил ссылки для бэка 2025-11-18 14:42:52 +04:00
ee157a05a1 правка сортировки, и теперь точно лабы - В С Ё 2025-10-23 14:17:45 +04:00
f4422f8a3c допка готова, остались отчёты 2025-10-21 20:30:37 +04:00
0253712e54 осталась допка по 6 лабе 2025-10-09 14:53:05 +04:00
edca1ca741 ИП 2 курс - В С Ё 2025-10-08 23:50:41 +04:00
1df9d6be2b круд работает 2025-10-05 15:10:33 +04:00
39aa30cc9e список, форма, свага
не работает редактирование
2025-10-04 19:18:40 +04:00
e126ac0b0b исправил ошибку с import 2025-09-05 14:34:17 +04:00
a0d1f62583 создал ветку, настроил окружение 2025-05-24 10:55:52 +04:00
8adf0a5727 четвертая лаба 2025-05-23 22:01:34 +04:00
38c52febe2 починил список и форму 2025-05-22 16:29:31 +04:00
1fef86cb17 теперь точно третья лаба готова 2025-05-14 15:38:23 +04:00
f9f430f92b третья лаба готова 2025-04-30 15:07:55 +04:00
38b22411cc остались иконки и форма 2025-04-29 20:40:50 +04:00
8fc03cb811 настроил окружение 2025-04-12 10:36:38 +04:00
59 changed files with 7830 additions and 382 deletions

42
.gitignore vendored
View File

@@ -1,14 +1,42 @@
# ---> VisualStudioCode
# 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
!.vscode/*.code-snippets
.history/*
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# 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
}

8
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"recommendations": [
"usernamehw.errorlens",
"AndersEAndersen.html-class-suggestions",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

18
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,18 @@
{
"configurations": [
{
"type": "chrome",
"name": "Debug",
"request": "launch",
"url": "http://localhost:5173"
},
{
"type": "node",
"name": "Start",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "start"],
"console": "integratedTerminal"
}
]
}

39
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,39 @@
{
"files.autoSave": "onFocusChange",
"files.eol": "\n",
"editor.detectIndentation": false,
"editor.formatOnType": false,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.sortImports": "explicit"
},
"editor.snippetSuggestions": "bottom",
"debug.toolBarLocation": "commandCenter",
"debug.showVariableTypes": true,
"errorLens.gutterIconsEnabled": true,
"errorLens.messageEnabled": false,
"prettier.tabWidth": 4,
"prettier.singleQuote": false,
"prettier.printWidth": 120,
"prettier.trailingComma": "es5",
"prettier.useTabs": false,
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View File

@@ -1,3 +1,17 @@
# PIbd-23_Sheymuhov_A.I._Internet_Programming
Установка зависимостей
Лабораторные работы по дисциплине "Интернет программирование"
```
npm install
```
Запуск в режиме разработки
```
npm start
```
Запуск для использования в продуктовой среде
```
npm run prod
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

80
database/data.json Normal file

File diff suppressed because one or more lines are too long

52
eslint.config.js Normal file
View File

@@ -0,0 +1,52 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier/flat";
import * as 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,
parserOptions: {
ecmaVersion: 2020,
},
},
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"],
},
},
];

20
index.html Normal file
View File

@@ -0,0 +1,20 @@
<!doctype html>
<html lang="ru" class="h-100">
<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="/kitty.svg" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" />
</head>
<body class="h-100">
<div class="h-100 body" 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/*"]
}

6463
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "int-prog",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "npm-run-all vite",
"vite": "vite",
"build": "vite build",
"serve": "http-server -p 3000 ./dist/",
"backend": "json-server database/data.json -p 5174",
"prod": "npm-run-all build serve",
"lint": "eslint ."
},
"dependencies": {
"bootstrap": "5.3.3",
"bootstrap-icons": "1.11.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.2"
},
"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",
"json-server": "^1.0.0-beta.3",
"npm-run-all": "^4.1.5",
"vite": "^6.3.6"
}
}

View File

@@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Мой аккаунт</title>
<link rel="stylesheet" href="styleStreamingService.css" />
</head>
<body>
<div class="header">
<div class="header-logo">
<a href="pageMain.html"><img src="Resources\КАЙФ.jpg" alt="Логотип"></a>
<h1>НАЗВАНИЕ СЕРВИСА</h1>
</div>
<nav class="navbar">
<a href="pageCategories.html">Категории</a>
<nav class="dropdown">
<span><a>Мой аккаунт ▾</a></span>
<nav class="features-menu">
<nav class="features-item"><a href="pageAccount.html">Настройки</a></nav>
<nav class="features-item"><a href="pageSubscriptions.html">Подписки</a></nav>
<nav class="features-item"><a href="pageSavedStreams.html">Сохраненные трансляции</a></nav>
</nav>
</nav>
</nav>
</div>
<div class="content">
<h2><em>Ваш никнейм</em></h2>
<div class="avatar"><img src="Resources\derzko.webp" alt="derzko"></div>
<p>ПОЛ МИЛЛИОНА ПОДПИЩИКОВ</p>
<div class="header-logo">
<div class="button">Выйти из аккаунта</div>
<div class="button">Сменить пароль</div>
</div>
</div>
<div class="footer">
<div class="footer-content">
<div class="company-name">RBCS CORP.</div>
<div class="footer-images">
<img src="Resources\vk_icon.png" alt="vk" class="footer-image">
<img src="Resources\tg_icon.png" alt="tg" class="footer-image">
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Популярные категории</title>
<link rel="stylesheet" href="styleStreamingService.css" />
</head>
<body>
<div class="header">
<div class="header-logo">
<a href="pageMain.html"><img src="Resources\КАЙФ.jpg" alt="Логотип"></a>
<h1>НАЗВАНИЕ СЕРВИСА</h1>
</div>
<nav class="navbar">
<a href="pageCategories.html">Категории</a>
<nav class="dropdown">
<span><a>Мой аккаунт ▾</a></span>
<nav class="features-menu">
<nav class="features-item"><a href="pageAccount.html">Настройки</a></nav>
<nav class="features-item"><a href="pageSubscriptions.html">Подписки</a></nav>
<nav class="features-item"><a href="pageSavedStreams.html">Сохраненные трансляции</a></nav>
</nav>
</nav>
</nav>
</div>
<div class="content">
<h2>Популярные категории</h2>
<ol>
<li>Общение</li>
<li>Казик</li>
<li>Стрелялки</li>
<li>пупупу</li>
</ol>
</div>
<div class="footer">
<div class="footer-content">
<div class="company-name">RBCS CORP.</div>
<div class="footer-images">
<img src="Resources\vk_icon.png" alt="vk" class="footer-image">
<img src="Resources\tg_icon.png" alt="tg" class="footer-image">
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,56 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Главная</title>
<link rel="stylesheet" href="styleStreamingService.css" />
</head>
<body>
<div class="header">
<div class="header-logo">
<a href="pageMain.html"><img src="Resources\КАЙФ.jpg" alt="Логотип"></a>
<h1>НАЗВАНИЕ СЕРВИСА</h1>
</div>
<nav class="navbar">
<a href="pageCategories.html">Категории</a>
<nav class="dropdown">
<span><a>Мой аккаунт ▾</a></span>
<nav class="features-menu">
<nav class="features-item"><a href="pageAccount.html">Настройки</a></nav>
<nav class="features-item"><a href="pageSubscriptions.html">Подписки</a></nav>
<nav class="features-item"><a href="pageSavedStreams.html">Сохраненные трансляции</a></nav>
</nav>
</nav>
</nav>
</div>
<div class="content">
<p>Сейчас в эфире</p>
<div class="photo-grid-container">
<div class="photo-grid">
<div class="photo-grid-item"><img src="Resources\cs2.webp" alt="cs2"></div>
<div class="photo-grid-item"><img src="Resources\derzko.webp" alt="derzko"></div>
<div class="photo-grid-item"><img src="Resources\stardew.webp" alt="stardew"></div>
<div class="photo-grid-item"><img src="Resources\teddy.jpg" alt="teddy"></div>
<div class="photo-grid-item"><img src="Resources\lofi_girl.jpg" alt="lofi_girl"></div>
</div>
</div>
<h3>Популярные каналы</h3>
<ul>
<li><a href="pageAccount.html" target="_blank"><em>ВЫ</em> самый популярный стример на данной платформе!!!</a></li>
<li>какой-то стример 1</li>
<li>какой-то стример 2</li>
<li>ммм <b>МАРМОК</b></li>
</ul>
</div>
<div class="footer">
<div class="footer-content">
<div class="company-name">RBCS CORP.</div>
<div class="footer-images">
<a href="https://vk.com/sheym_not_shame" target="_blank"><img src="Resources\vk_icon.png" alt="vk" class="footer-image"></a>
<a href="https://t.me/sheymuh" target="_blank"><img src="Resources\tg_icon.png" alt="tg" class="footer-image"></a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,55 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Сохранённые трансляции</title>
<link rel="stylesheet" href="styleStreamingService.css" />
</head>
<body>
<div class="header">
<div class="header-logo">
<a href="pageMain.html"><img src="Resources\КАЙФ.jpg" alt="Логотип"></a>
<h1>НАЗВАНИЕ СЕРВИСА</h1>
</div>
<nav class="navbar">
<a href="pageCategories.html">Категории</a>
<nav class="dropdown">
<span><a>Мой аккаунт ▾</a></span>
<nav class="features-menu">
<nav class="features-item"><a href="pageAccount.html">Настройки</a></nav>
<nav class="features-item"><a href="pageSubscriptions.html">Подписки</a></nav>
<nav class="features-item"><a href="pageSavedStreams.html">Сохраненные трансляции</a></nav>
</nav>
</nav>
</nav>
</div>
<div class="content">
<h2>Запланированные трансляции</h2>
<div class="photo-grid-container">
<div class="photo-grid">
<div class="photo-grid-item"><img src="Resources\стрим ксго.webp" alt="стрим ксго"></div>
<div class="photo-grid-item"><img src="Resources\goats.png" alt="goats"></div>
<div class="photo-grid-item"><img src="Resources\папаня.jpg" alt="папаня"></div>
</div>
</div>
<h2>Смотреть позже</h2>
<div class="photo-grid-container">
<div class="photo-grid">
<div class="photo-grid-item"><img src="Resources\2016.jpeg" alt="стрим ксго"></div>
<div class="photo-grid-item"><img src="Resources\асмр человек паук.webp" alt="асмр"></div>
<div class="photo-grid-item"><img src="Resources\резня.jpg" alt="резня"></div>
</div>
</div>
</div>
<div class="footer">
<div class="footer-content">
<div class="company-name">RBCS CORP.</div>
<div class="footer-images">
<img src="Resources\vk_icon.png" alt="vk" class="footer-image">
<img src="Resources\tg_icon.png" alt="tg" class="footer-image">
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,48 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Подписки</title>
<link rel="stylesheet" href="styleStreamingService.css" />
</head>
<body>
<div class="header">
<div class="header-logo">
<a href="pageMain.html"><img src="Resources\КАЙФ.jpg" alt="Логотип"></a>
<h1>НАЗВАНИЕ СЕРВИСА</h1>
</div>
<nav class="navbar">
<a href="pageCategories.html">Категории</a>
<nav class="dropdown">
<span><a>Мой аккаунт ▾</a></span>
<nav class="features-menu">
<nav class="features-item"><a href="pageAccount.html">Настройки</a></nav>
<nav class="features-item"><a href="pageSubscriptions.html">Подписки</a></nav>
<nav class="features-item"><a href="pageSavedStreams.html">Сохраненные трансляции</a></nav>
</nav>
</nav>
</nav>
</div>
<div class="content">
<h2>Ваши подписки</h2>
<ol>
<li>НОРМ канал</li>
<div class="subButtons"><div class="button">Вы подписаны</div></div>
<li>САМЫЙ КРУТОЙ КАНАЛ</li>
<div class="subButtons"><div class="button blue-button">Вы спонсор</div></div>
<li>ПРОСТО КРУТОЙ канал</li>
<div class="subButtons"><div class="button">Вы подписаны</div></div>
</ol>
</div>
<div class="footer">
<div class="footer-content">
<div class="company-name">RBCS CORP.</div>
<div class="footer-images">
<img src="Resources\vk_icon.png" alt="vk" class="footer-image">
<img src="Resources\tg_icon.png" alt="tg" class="footer-image">
</div>
</div>
</div>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 201 KiB

BIN
public/STREAM.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 642 KiB

After

Width:  |  Height:  |  Size: 642 KiB

BIN
public/king.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

15
public/kitty.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 393 KiB

BIN
public/mmmMARMOK.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

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

@@ -0,0 +1,29 @@
import { HashRouter, Route, Routes } from "react-router-dom";
import { MainLayout } from "./layout/MainLayout";
import { AccountPage } from "./pages/AccountPage";
import { CategoryPage } from "./pages/CategoryPage";
import { FormPage } from "./pages/FormPage";
import { MainPage } from "./pages/MainPage";
import { SavedStreamsPage } from "./pages/SavedStreamsPage";
import { SubscriptionsPage } from "./pages/SubscriptionsPage";
import { NotFoundPage } from "./pages/NotFoundPage";
export const App = () => {
return (
<HashRouter>
<Routes>
<Route element={<MainLayout />}>
<Route index element={<MainPage />} />
<Route path="/form" element={<FormPage />} />
<Route path="/form/:id" element={<FormPage />} />
<Route path="/category" element={<CategoryPage />} />
<Route path="/account" element={<AccountPage />} />
<Route path="/subscriptions" element={<SubscriptionsPage />} />
<Route path="/savedStreams" element={<SavedStreamsPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</HashRouter>
);
};

16
src/app/api/category.js Normal file
View File

@@ -0,0 +1,16 @@
const BASE = "http://localhost:8080/api/1.0/category";
export const fetchCategories = () => fetch(`${BASE}`).then((r) => r.json());
export const fetchCategory = (id) => fetch(`${BASE}/${id}`).then((r) => r.json());
export const createCategory = (g) =>
fetch(BASE, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(g),
}).then((r) => r.json());
export const updateCategory = (id, g) =>
fetch(`${BASE}/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(g),
}).then((r) => r.json());
export const deleteCategory = (id) => fetch(`${BASE}/${id}`, { method: "DELETE" });

16
src/app/api/playlist.js Normal file
View File

@@ -0,0 +1,16 @@
const BASE = "http://localhost:8080/api/1.0/playlist";
export const fetchPlaylists = () => fetch(`${BASE}`).then((r) => r.json());
export const fetchPlaylist = (id) => fetch(`${BASE}/${id}`).then((r) => r.json());
export const createPlaylist = (p) =>
fetch(BASE, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(p),
}).then((r) => r.json());
export const updatePlaylist = (id, p) =>
fetch(`${BASE}/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(p),
}).then((r) => r.json());
export const deletePlaylist = (id) => fetch(`${BASE}/${id}`, { method: "DELETE" });

24
src/app/api/stream.js Normal file
View File

@@ -0,0 +1,24 @@
const BASE = "http://localhost:8080/api/1.0/stream";
export const fetchStreams = () => fetch(`${BASE}`).then((r) => r.json());
export const fetchStream = (id) => fetch(`${BASE}/${id}`).then((r) => r.json());
export const createStream = (s) =>
fetch(BASE, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(s),
}).then((r) => r.json());
export const updateStream = (id, s) =>
fetch(`${BASE}/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(s),
}).then((r) => r.json());
export const deleteStream = (id) =>
fetch(`${BASE}/${id}`, {
method: "DELETE",
}).then((r) => r.json());

View File

@@ -0,0 +1,27 @@
export const StreamCards = ({ stream, onEdit, onDelete }) => {
return (
<div className="card stream-card" style={{ maxWidth: '100%' }}>
<div className="d-flex justify-content-center align-items-center p-3">
<img
src={stream.image}
className="card-img-top w-100"
alt={stream.name}
style={{
height: '250px',
objectFit: 'cover'
}}
/>
</div>
<div className="card-body d-flex flex-column">
<h5 className="card-title">Название: {stream.name}</h5>
<h6 className="card-subtitle text-muted">Описание: {stream.description}</h6>
<p className="card-text">Плейлист: {stream.playlist?.name || "Не найден"}</p>
<p className="card-text">Категория: <em>{stream.category?.name || "Не найден"}</em></p>
<div className="card-buttons">
<button className="btn btn-warning" onClick={onEdit}>Редактировать</button>
<button className="btn btn-danger" onClick={onDelete}>Удалить</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,151 @@
import { useEffect, useState } from 'react';
import * as CategoryAPI from '../api/category';
import * as PlaylistAPI from '../api/playlist';
import * as API from '../api/stream';
export default function StreamForm({ id, onSuccess }) {
const [name, setName] = useState('');
const [image, setImage] = useState('');
const [description, setDesc] = useState('');
const [playlistId, setPlaylist] = useState('');
const [categoryId, setCategory] = useState('');
const [playlists, setPlaylists] = useState([]);
const [categories, setCategories] = useState([]);
console.log('StreamForm - id:', id);
useEffect(() => {
console.log('useEffect triggered, id:', id);
CategoryAPI.fetchCategories().then(setCategories);
PlaylistAPI.fetchPlaylists().then(setPlaylists);
if (id) {
String(id);
API.fetchStream(id).then((s) => {
console.log('Fetching stream with id:', id);
setName(s.name);
setImage(s.image);
setDesc(s.description);
setPlaylist(String(s.playlistId));
setCategory(String(s.categoryId));
}).catch(error => {
console.error('Error fetching stream:', error);
});
}
}, [id]);
async function handleSubmit(e) {
e.preventDefault();
const stream = {
name,
image,
description,
playlistId: String(playlistId), // Преобразуем в строку
categoryId: String(categoryId) // Преобразуем в строку
};
if (id) {
await API.updateStream(String(id), stream);
}
else await API.createStream(stream);
onSuccess();
}
function handleFileChange(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onloadend = () => {
// @ts-ignore
setImage(reader.result);
};
reader.readAsDataURL(file);
}
return (
<form onSubmit={handleSubmit} className="container py-4">
<h2>{id ? 'Редактировать' : 'Добавить'} трансляцию</h2>
<p></p>
<div className="mb-3">
<label className="form-label">Название </label>
<input
type="text"
className="form-control"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div className="mb-3">
<label className="form-label">Описание </label>
<textarea
className="form-control"
value={description}
onChange={(e) => setDesc(e.target.value)}
required
></textarea>
</div>
<div className="mb-3">
<label className="form-label">Изображение (файл) </label>
<input
type="file"
className="form-control"
onChange={handleFileChange}
accept="image/*"
/>
</div>
{image && (
<div className="mb-3">
<label className="form-label">Предпросмотр:</label><br />
<img src={image} alt="preview" className="img-thumbnail" style={{ maxHeight: "200px" }} />
</div>
)}
<div className="mb-3">
<label className="form-label">Категория </label>
<select
className="form-select"
value={categoryId}
// @ts-ignore
onChange={(e) => setCategory(e.target.value)}
required
>
<option value="">-- Выберите категорию --</option>
{categories.map((с) => (
<option key={с.id} value={String(с.id)}>
{с.name}
</option>
))}
</select>
</div>
<div className="mb-3">
<label className="form-label">Плейлист </label>
<select
className="form-select"
value={playlistId}
// @ts-ignore
onChange={(e) => setPlaylist(e.target.value)}
required
>
<option value="">-- Выберите плейлист --</option>
{playlists.map((p) => (
<option key={p.id} value={String(p.id)}>
{p.name}
</option>
))}
</select>
</div>
<button type="submit" className="btn btn-primary">
{id ? 'Сохранить' : 'Создать'}
</button>
</form>
);
}

View File

@@ -0,0 +1,13 @@
import { StreamCards } from "./StreamCards";
export const StreamsList = ({ streams, onEdit, onDelete }) => {
return (
<div className="row">
{streams.map(s => (
<div key={s.id} className="col-sm-6 col-lg-4">
<StreamCards stream={s} onEdit={()=>onEdit(s)} onDelete={()=>onDelete(String(s.id))} />
</div>
))}
</div>
);
}

121
src/app/hooks/useStreams.js Normal file
View File

@@ -0,0 +1,121 @@
import { useEffect, useState } from "react";
import * as CategoryAPI from "../api/category";
import * as PlaylistAPI from "../api/playlist";
import * as API from "../api/stream";
export function useStreams() {
const [streams, setStreams] = useState([]);
const [categories, setCategories] = useState([]);
const [playlists, setPlaylists] = useState([]);
const [filters, setFilters] = useState({
categoryId: "",
playlistId: "",
});
const [loading, setLoading] = useState(true);
useEffect(() => {
load();
}, []);
async function load() {
try {
setLoading(true);
const [streamsData, categoriesData, playlistsData] = await Promise.all([
API.fetchStreams(),
CategoryAPI.fetchCategories(),
PlaylistAPI.fetchPlaylists(),
]);
console.log("Streams data:", streamsData);
console.log("Categories data:", categoriesData);
console.log("Playlists data:", playlistsData);
const extended = streamsData.map((stream) => ({
...stream,
id: String(stream.id),
playlistId: stream.playlist ? String(stream.playlist.id) : "",
categoryId: stream.category ? String(stream.category.id) : "",
category: stream.category,
playlist: stream.playlist,
}));
setStreams(extended);
setCategories(categoriesData || []);
setPlaylists(playlistsData || []);
} catch (error) {
console.error("Error loading data:", error);
setCategories([]);
setPlaylists([]);
} finally {
setLoading(false);
}
}
function applyFilters(categoryId = "", playlistId = "") {
setFilters({
categoryId: String(categoryId),
playlistId: String(playlistId),
});
}
function resetFilters() {
setFilters({
categoryId: "",
playlistId: "",
});
}
const filteredStreams = streams.filter((stream) => {
const matchesCategory = !filters.categoryId || String(stream.categoryId) === filters.categoryId;
const matchesPlaylist = !filters.playlistId || String(stream.playlistId) === filters.playlistId;
return matchesCategory && matchesPlaylist;
});
function sortAsc() {
const sorted = [...streams].sort((a, b) => a.name.localeCompare(b.name));
setStreams(sorted);
}
function sortDesc() {
const sorted = [...streams].sort((a, b) => b.name.localeCompare(a.name));
setStreams(sorted);
}
async function remove(id) {
await API.deleteStream(String(id));
await load();
}
async function save(stream) {
const saveStream = {
name: stream.name,
image: stream.image,
description: stream.description,
playlistId: Number(stream.playlistId),
categoryId: Number(stream.categoryId),
};
if (stream.id) {
await API.updateStream(String(stream.id), saveStream);
} else {
await API.createStream(saveStream);
}
await load();
}
return {
streams: filteredStreams,
allStreams: streams,
categories,
playlists,
remove,
save,
sortAsc,
sortDesc,
applyFilters,
resetFilters,
currentFilters: filters,
loading,
};
}

17
src/app/layout/Footer.jsx Normal file
View File

@@ -0,0 +1,17 @@
export const Footer = () => {
return (
<footer className="footer d-flex justify-content-center align-items-center p-3 position-relative">
<div className="company-name text-center">
RBCS CORP. {new Date().getFullYear()} <i className="bi bi-c-circle"></i>
</div>
<div className="footer-icons d-flex gap-3 position-absolute end-0 me-3">
<a href="https://vk.com/sheym_not_shame" target="_blank" rel="noreferrer">
<img src="/vk_icon.png" alt="vk" />
</a>
<a href="https://t.me/sheymuh" target="_blank" rel="noreferrer">
<img src="/tg_icon.png" alt="tg" />
</a>
</div>
</footer>
);
};

33
src/app/layout/Header.jsx Normal file
View File

@@ -0,0 +1,33 @@
import { Link } from 'react-router-dom';
export const Header = () => {
return (
<header className="d-flex align-items-center justify-content-between position-sticky h-60 p-3">
<div className="header-logo d-flex align-items-center g-3">
<Link to="/">
<img src="/КАЙФ.jpg" alt="Логотип" />
</Link>
<h1>СТРИМЫ ОНЛАЙН БЕСПЛАТНО</h1>
</div>
<nav className="navbar d-flex align-items-center justify-content-center me-3 g-3 column-gap-2">
<Link to="/category">Категории</Link>
<div className="dropdown position-relative">
<span>
<a>Мой аккаунт </a>
</span>
<div className="features-menu">
<div className="features-item">
<Link to="/account">Настройки</Link>
</div>
<div className="features-item">
<Link to="/subscriptions">Подписки</Link>
</div>
<div className="features-item">
<Link to="/savedStreams">Сохраненные трансляции</Link>
</div>
</div>
</div>
</nav>
</header>
);
};

View File

@@ -0,0 +1,15 @@
import { Outlet } from "react-router-dom";
import { Footer } from "./Footer";
import { Header } from "./Header";
export const MainLayout = () => {
return (
<>
<Header />
<main className="main flex-grow-1 p-2">
<Outlet />
</main>
<Footer />
</>
);
};

View File

@@ -0,0 +1,132 @@
import { useNavigate } from 'react-router-dom';
import { StreamsList } from '../components/StreamsList';
import { useStreams } from '../hooks/useStreams';
export const AccountPage = () => {
const {
streams,
categories,
playlists,
remove,
sortAsc,
sortDesc,
applyFilters,
resetFilters,
currentFilters,
loading,
} = useStreams();
const navigate = useNavigate();
const handleCategoryFilter = (categoryId) => {
applyFilters(categoryId, currentFilters.playlistId);
};
const handlePlaylistFilter = (playlistId) => {
applyFilters(currentFilters.categoryId, playlistId);
};
return (
<main className="flex-grow-1 pt-2">
<h2><em>Ваш никнейм</em> <i className="bi bi-patch-check-fill"></i></h2>
<div className="avatar d-flex"><img src="/derzko.webp" alt="derzko" /></div>
<p>ПОЛ МИЛЛИОНА ПОДПИЩИКОВ</p>
{/* Секция фильтров */}
<div className="filters-section mb-4 p-3 border rounded">
<h4>Фильтры</h4>
<div className="row">
<div className="col-md-4">
<label className="form-label">Фильтр по категории:</label>
<select
className="form-select"
value={currentFilters.categoryId}
onChange={(e) => handleCategoryFilter(e.target.value)}
disabled={loading}
>
<option value="">Все категории</option>
{categories.map(category => (
<option key={category.id} value={String(category.id)}>
{category.name || `Категория ${category.id}`}
</option>
))}
</select>
{categories.length === 0 && !loading && (
<div className="text-danger small">Нет доступных категорий</div>
)}
</div>
<div className="col-md-4">
<label className="form-label">Фильтр по плейлисту:</label>
<select
className="form-select"
value={currentFilters.playlistId}
onChange={(e) => handlePlaylistFilter(e.target.value)}
disabled={loading}
>
<option value="">Все плейлисты</option>
{playlists.map(playlist => (
<option key={playlist.id} value={String(playlist.id)}>
{playlist.name || `Плейлист ${playlist.id}`}
</option>
))}
</select>
{playlists.length === 0 && !loading && (
<div className="text-danger small">Нет доступных плейлистов</div>
)}
</div>
<div className="col-md-4 d-flex align-items-end">
<button
className="btn btn-outline-secondary w-100"
onClick={resetFilters}
disabled={loading}
>
Сбросить фильтры
</button>
</div>
</div>
</div>
<div className="buttons d-flex align-items-center gap-3 mb-3">
<h2 className="mb-0">Начать новую трансляцию</h2>
<button
className="btn btn-success"
onClick={() => navigate('/form')}
>
Добавить
</button>
<button
className="btn btn-primary"
onClick={sortAsc}
>
Отсортировать по возрастанию
</button>
<button
className="btn btn-primary"
onClick={sortDesc}
>
Отсортировать по убыванию
</button>
</div>
<div id="streamsList" className="row mt-1">
<h3>Мои трансляции {streams.length > 0 && `(${streams.length})`}</h3>
{loading ? (
<div className="alert alert-info">Загрузка данных...</div>
) : streams.length === 0 ? (
<div className="alert alert-info">
Нет трансляций, соответствующих выбранным фильтрам
</div>
) : (
<StreamsList
streams={streams}
onEdit={(s) => navigate(`/form/${s.id}`)}
onDelete={remove}
/>
)}
</div>
</main>
);
};

View File

@@ -0,0 +1,13 @@
export const CategoryPage = () => {
return (
<main className="flex-grow-1 pt-2">
<h2>Популярные категории</h2>
<ul>
<li>Общение</li>
<li>Казик</li>
<li>Стрелялки</li>
<li>пупупу</li>
</ul>
</main>
)
}

View File

@@ -0,0 +1,11 @@
import { useNavigate, useParams } from 'react-router-dom';
import StreamForm from '../components/StreamForm';
export const FormPage = () => {
const { id } = useParams();
const navigate = useNavigate();
console.log('FormPage - id:', id);
return <StreamForm id={id} onSuccess={() => navigate('/account')} />;
};

111
src/app/pages/MainPage.jsx Normal file
View File

@@ -0,0 +1,111 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
export const MainPage = () => {
const [savedImages, setSavedImages] = useState([]);
const initialStreams = [
{
id: 'cs2',
name: 'CS2',
image: 'https://steamuserimages-a.akamaihd.net/ugc/2462990917964003785/9E09A87AE9B299BC1F0FC1CBA9F20DB16289442A/?imw=512&imh=298&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true'
},
{
id: 'derzko',
name: 'Dersko',
image: 'https://avatars.mds.yandex.net/i?id=7839e8d6f40309bdce67fac62990d108_sr-10636981-images-thumbs&n=13'
},
{
id: 'stardew',
name: 'Stardew Valley',
image: 'https://vkplay.ru/pre_0x736_resize/hotbox/content_files/news/2020/02/12/fe52b98a1367439ea2be293fcf48224a.jpg?quality=85'
},
{
id: 'teddy',
name: 'Teddy',
image: 'https://avatars.mds.yandex.net/i?id=c111e02e6999cca7c4a7aa47f00a2ab3c384b6dd-5884537-images-thumbs&n=13'
},
{
id: 'lofi_girl',
name: 'Lofi Girl',
image: 'https://i.pinimg.com/736x/a8/f1/c0/a8f1c04546867fbcd5eccd41c115fb51.jpg'
}
];
const popularChannels = [
{ name: 'ВЫ самый популярный стример на данной платформе!!!', link: '/account' },
{ name: 'какой-то стример 1' },
{ name: 'какой-то стример 2' },
{ name: 'ммм МАРМОК' }
];
const handleSaveStreams = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const selectedImages = Array.from(formData.getAll('images'));
setSavedImages(selectedImages.map(url => ({ url })));
};
return (
<main className="flex-grow-1 pt-2">
<h2>Сейчас в эфире <i className="bi bi-cast"></i></h2>
<form id="imageForm" className="mb-4" onSubmit={handleSaveStreams}>
<div className="photo-grid-container d-flex justify-content-center mb-2">
<div className="photo-grid d-flex align-items-center flex-wrap w-100">
{initialStreams.map(stream => (
<div key={stream.id} className="photo-grid-item">
<input
type="checkbox"
name="images"
value={stream.image}
id={stream.id}
/>
<label htmlFor={stream.id}>
<img src={stream.image} alt={stream.name} />
</label>
</div>
))}
</div>
</div>
<button type="submit" className="btn btn-primary">Смотреть позже</button>
</form>
<h3>Популярные каналы <i className="bi bi-patch-check-fill"></i></h3>
<ul>
{popularChannels.map((channel, index) => (
<li key={index}>
{channel.link ? (
<Link to={channel.link}>
<em>{channel.name}</em>
</Link>
) : (
channel.name
)}
</li>
))}
</ul>
<h2>Смотреть позже <i className="bi bi-clock-fill"></i></h2>
<div className="photo-grid-container d-flex justify-content-center">
<div className="photo-grid d-flex align-items-center flex-wrap w-100" id="savedImagesGrid">
{savedImages.length > 0 ? (
savedImages.map((image, index) => (
<div key={index} className="photo-grid-item">
<img src={image.url} alt="сохраненное изображение" />
</div>
))
) : (
<div className="photo-grid-item">
<img
src="https://sun9-27.userapi.com/impf/c9811/u99622377/d_9475926f.jpg?quality=96&as=50x50,100x100&sign=a4fcc81d8c851f41a7f85dea825afc66&u=pHWjezk_9pOPyRtoH8161qsxD963pzSE2bk8P8vDAyE&cs=100x100"
alt="pusto"
/>
</div>
)}
</div>
</div>
</main>
);
};

View File

@@ -0,0 +1,12 @@
import { Link } from "react-router-dom";
export const NotFoundPage = () => {
return (
<>
<h5>Страница не найдена</h5>
<Link className="nav-link" to="/">
Вернуться на главную
</Link>
</>
);
};

View File

@@ -0,0 +1,22 @@
export const SavedStreamsPage = () => {
return (
<main className="flex-grow-1 pt-2">
<h2>Вам понравилось <i className="bi bi-balloon-heart"></i></h2>
<div className="photo-grid-container d-flex justify-content-center">
<div className="photo-grid d-flex align-items-center flex-wrap w-100" id="savedImagesGrid">
<div className="photo-grid-item"><img src="/2016.jpeg" alt="стрим ксго" /></div>
<div className="photo-grid-item"><img src="/асмр человек паук.webp" alt="асмр" /></div>
<div className="photo-grid-item"><img src="/резня.jpg" alt="резня" /></div>
</div>
</div>
<h2>Запланированные трансляции <i className="bi bi-calendar-event"></i></h2>
<div className="photo-grid-container d-flex justify-content-center">
<div className="photo-grid d-flex align-items-center flex-wrap w-100">
<div className="photo-grid-item"><img src="/стрим ксго.webp" alt="стрим ксго" /></div>
<div className="photo-grid-item"><img src="/goats.png" alt="goats" /></div>
<div className="photo-grid-item"><img src="/папаня.jpg" alt="папаня" /></div>
</div>
</div>
</main>
)
}

View File

@@ -0,0 +1,15 @@
export const SubscriptionsPage = () => {
return (
<main className="flex-grow-1 pt-2">
<h2>Ваши подписки <i className="bi bi-bookmark-heart-fill"></i></h2>
<ol>
<li>НОРМ канал</li>
<div className="subButtons d-flex"><div className="button">Вы подписаны</div></div>
<li>САМЫЙ КРУТОЙ КАНАЛ</li>
<div className="subButtons d-flex"><div className="button blue-button">Вы спонсор</div></div>
<li>ПРОСТО КРУТОЙ канал</li>
<div className="subButtons d-flex"><div className="button">Вы подписаны</div></div>
</ol>
</main>
)
}

10
src/index.jsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./app/App";
import "./styles.css";
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@@ -1,47 +1,39 @@
body {
display: flex;
flex-direction: column;
min-height: 100vh;
font-family: "Helvetica", "Arial", sans-serif;
font-size: 18px;
:root {
--my-bg-color: #fff;
--my-fg-color: #000;
--my-scale-value: 4px;
}
* {
padding: 0;
margin: 0;
padding: 0;
background-color: #ade8f4;
}
h1 {
font-size: 1.5em;
margin: 20px auto;
}
h2 {
font-size: 1em;
margin: 20px auto;
text-align: left;
box-sizing: border-box;
}
/* Мобильное устройство (ширина области отображения от 0 до 400px)*/
@media only screen and (max-width: 400px) {
h1 {
font-size: 1em;;
font-size: 1em;
}
h2, h3, p {
h2,
h3,
p {
text-align: center;
}
.header {
gap: 20px;
}
.navbar {
flex-direction: column;
align-self: center;
row-gap: 5px;
}
.navbar a {
height: 10px;
height: 30px;
}
.dropdown:hover .features-menu {
height: max-content;
row-gap: 10px;
}
.features-item a {
height: 40px;
text-align: start;
}
.avatar {
justify-content: center;
@@ -63,13 +55,204 @@ h2 {
body {
justify-content: center;
}
.header {
gap: 20px;
}
}
.subButtons {
html {
font-size: 18px;
background-color: var(--my-bg-color);
background-color: #ade8f4;
color: var(--my-fg-color);
font-family: Arial, Helvetica, sans-serif;
}
.body {
display: flex;
background-color: #ade8f4;
flex-direction: column;
overflow-x: hidden;
}
.header,
.footer {
display: flex;
width: 100%;
flex-direction: row;
align-items: center;
height: 64px;
border-bottom: 1px solid black;
flex-shrink: 0;
}
.header {
background-color: #00b4d8;
}
.footer {
height: 48px;
border-bottom: none;
justify-content: center;
color: #ade8f4;
background-color: #023047;
border-top: 1px solid black;
}
.header .nav-link {
padding-right: 5px;
}
.nav-link.active {
font-weight: bold;
}
.nav-link {
color: black;
text-decoration: underline;
cursor: pointer;
}
.nav-link:hover {
text-decoration: none;
}
.main {
width: 100%;
}
.h-100 {
height: 100%;
}
.p-2 {
padding: calc(var(--my-scale-value) * 2);
}
.pt-2 {
padding-top: calc(var(--my-scale-value) * 2);
}
.pb-2 {
padding-bottom: calc(var(--my-scale-value) * 2);
}
.my-2 {
margin-top: calc(var(--my-scale-value) * 2);
margin-bottom: calc(var(--my-scale-value) * 2);
}
.mx-2 {
margin-left: calc(var(--my-scale-value) * 2);
margin-right: calc(var(--my-scale-value) * 2);
}
.error {
color: #ec2525;
}
.input-group {
display: flex;
flex-direction: column;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.25em;
}
h3 {
font-size: 1.1em;
}
h4 {
font-size: 1em;
}
h5 {
font-size: 0.95em;
}
h6 {
font-size: 0.9em;
}
.w-100 {
width: 100%;
}
.d-none {
display: none;
}
.d-block {
display: block;
}
.d-flex {
display: flex;
}
.flex-grow-1 {
flex-grow: 1 !important;
}
.flex-direction-column {
flex-direction: column;
}
.justify-content-right {
justify-content: right;
}
.justify-content-center {
justify-content: center;
}
.text-align-center {
text-align: center;
}
.card {
padding: 5px;
border: 1px solid var(--my-fg-color);
}
.container,
.panel {
padding: 5px;
width: 100%;
}
.panel,
.table {
padding: 5px;
border: 1px solid var(--my-fg-color);
width: 100%;
}
.table {
border: none;
}
.panel-header {
cursor: pointer;
}
.stream-card {
display: flex;
flex-direction: column;
height: auto;
width: auto;
}
.stream-card .card-body {
flex: 1;
display: flex;
flex-direction: row;
}
.stream-card .card-buttons {
margin-top: auto;
}
.button {
@@ -97,48 +280,41 @@ h2 {
background: #016296;
}
.header {
display: flex;
top: 0;
left: 0;
height: 60px;
position: sticky;
background-color: #00b4d8;
color: #081c15;
align-items: center;
justify-content: space-between;
padding: 10px;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
.photo-grid img {
width: 256px;
height: 200px;
}
.header-logo {
display: flex;
.photo-grid-item {
height: 200px;
background-color: #5995da;
align-items: center;
gap: 15px;
justify-self: center;
border: 2px solid #fff;
}
header {
background-color: #00b4d8;
color: #081c15;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
}
.header-logo img {
width: 50px;
height: auto;
}
.navbar {
display: flex;
justify-content: center;
column-gap: 15px;
margin-right: 20px;
margin: 5px;
}
.navbar a {
display: flex;
text-decoration: none;
width: fit-content;
align-items: center;
justify-content: center;
padding: 8px 15px;
width: fit-content;
color: #081c15;
font-weight: bold;
background: #90e0ef;
padding: 8px 15px;
border-radius: 5px;
}
@@ -148,7 +324,6 @@ h2 {
.dropdown {
width: max-content;
position: relative;
}
.dropdown > span {
@@ -175,70 +350,3 @@ h2 {
border-radius: 5px;
background: #48cae4;
}
.content {
flex: 1;
padding: 10px 20px;
}
.avatar {
display: flex;
}
.photo-grid-container {
display: flex;
justify-content: center;
}
.photo-grid {
width: 100%;
display: flex;
align-items: center;
flex-wrap: wrap;
flex-direction: row;
}
.photo-grid img {
width: 256px;
height: 200px;
}
.photo-grid-item {
height: 200px;
background-color: #5995da;
align-items: center;
justify-self: center;
border: 2px solid #fff;
}
.footer {
overflow: hidden;
flex-shrink: 1;
height: 80px;
background-color: #023047;
text-align: center;
padding: 0 40px;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.company-name {
flex: 1; /* Занимает доступное пространство */
color: #ade8f4;
}
.footer-images {
display: flex;
gap: 20px;
padding: 10px;
}
.footer-image {
width: 60px;
height: auto;
}

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()],
});