Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9f91741bb | |||
| 4d5eeb0459 | |||
| ee157a05a1 | |||
| f4422f8a3c | |||
| 0253712e54 | |||
| edca1ca741 | |||
| 1df9d6be2b | |||
| 39aa30cc9e | |||
| e126ac0b0b | |||
| a0d1f62583 | |||
| 8adf0a5727 | |||
| 38c52febe2 | |||
| 1fef86cb17 | |||
| f9f430f92b | |||
| 38b22411cc | |||
| 8fc03cb811 | |||
| a9bc51d17a | |||
| 49435b1342 |
42
.gitignore
vendored
@@ -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
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"singleQuote": false,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false
|
||||
}
|
||||
8
.vscode/extensions.json
vendored
Normal 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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
18
README.md
@@ -1,3 +1,17 @@
|
||||
# PIbd-23_Sheymuhov_A.I._Internet_Programming
|
||||
Установка зависимостей
|
||||
|
||||
Лабораторные работы по дисциплине "Интернет программирование"
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Запуск в режиме разработки
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Запуск для использования в продуктовой среде
|
||||
|
||||
```
|
||||
npm run prod
|
||||
```
|
||||
|
||||
80
database/data.json
Normal file
52
eslint.config.js
Normal 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
@@ -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
@@ -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
41
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
public/2016.jpeg
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
public/STREAM.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
public/cs2.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/derzko.webp
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
public/goats.png
Normal file
|
After Width: | Height: | Size: 642 KiB |
BIN
public/king.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
15
public/kitty.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/lofi_girl.jpg
Normal file
|
After Width: | Height: | Size: 393 KiB |
BIN
public/mmmMARMOK.jpg
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
public/stardew.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/teddy.jpg
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
public/tg_icon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/vk_icon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/КАЙФ.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
public/асмр человек паук.webp
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
public/крутой кот.jpg
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
public/папаня.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/резня.jpg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
public/стрим ксго.webp
Normal file
|
After Width: | Height: | Size: 83 KiB |
29
src/app/App.jsx
Normal 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
@@ -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
@@ -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
@@ -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());
|
||||
27
src/app/components/StreamCards.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
151
src/app/components/StreamForm.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
13
src/app/components/StreamsList.jsx
Normal 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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
15
src/app/layout/MainLayout.jsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
132
src/app/pages/AccountPage.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
13
src/app/pages/CategoryPage.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
11
src/app/pages/FormPage.jsx
Normal 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
@@ -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>
|
||||
);
|
||||
};
|
||||
12
src/app/pages/NotFoundPage.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const NotFoundPage = () => {
|
||||
return (
|
||||
<>
|
||||
<h5>Страница не найдена</h5>
|
||||
<Link className="nav-link" to="/">
|
||||
Вернуться на главную
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
22
src/app/pages/SavedStreamsPage.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
15
src/app/pages/SubscriptionsPage.jsx
Normal 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
@@ -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>
|
||||
);
|
||||
352
src/styles.css
Normal file
@@ -0,0 +1,352 @@
|
||||
:root {
|
||||
--my-bg-color: #fff;
|
||||
--my-fg-color: #000;
|
||||
--my-scale-value: 4px;
|
||||
}
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Мобильное устройство (ширина области отображения от 0 до 400px)*/
|
||||
@media only screen and (max-width: 400px) {
|
||||
h1 {
|
||||
font-size: 1em;
|
||||
}
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
.navbar {
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
row-gap: 5px;
|
||||
}
|
||||
.navbar a {
|
||||
height: 30px;
|
||||
}
|
||||
.dropdown:hover .features-menu {
|
||||
height: max-content;
|
||||
}
|
||||
.features-item a {
|
||||
height: 40px;
|
||||
text-align: start;
|
||||
}
|
||||
.avatar {
|
||||
justify-content: center;
|
||||
}
|
||||
.avatar img {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
}
|
||||
.photo-grid {
|
||||
justify-content: center;
|
||||
}
|
||||
.subButtons {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Планшет (ширина области отображения от 401 до 960px)*/
|
||||
@media only screen and (min-width: 401px) and (max-width: 960px) {
|
||||
body {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
color: #081c15;
|
||||
background-color: #90e0ef;
|
||||
font-weight: bold;
|
||||
padding: 10px 5px;
|
||||
text-align: center;
|
||||
border: 2px solid #5d6063;
|
||||
border-radius: 5px;
|
||||
width: 150px;
|
||||
box-sizing: border-box;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.blue-button {
|
||||
background-color: #0077b6;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #48cae4;
|
||||
}
|
||||
|
||||
.blue-button:hover {
|
||||
background: #016296;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #00b4d8;
|
||||
color: #081c15;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.header-logo img {
|
||||
width: 50px;
|
||||
height: auto;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 15px;
|
||||
width: fit-content;
|
||||
color: #081c15;
|
||||
font-weight: bold;
|
||||
background: #90e0ef;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.navbar a:hover {
|
||||
background: #48cae4;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.dropdown > span {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown:hover .features-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #90e0ef;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.features-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.features-item:hover {
|
||||
border-radius: 5px;
|
||||
background: #48cae4;
|
||||
}
|
||||
6
vite.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||