first commit

This commit is contained in:
shaman_lesnoy 2024-12-29 01:39:52 +03:00
commit 9e4041cd0b
33 changed files with 1631162 additions and 0 deletions

174
frontend/main.css Normal file
View file

@ -0,0 +1,174 @@
body {
background-color: #1B2838;
color: white;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: flex-start;
height: 100vh;
overflow: auto;
}
.main-container {
display: flex;
justify-content: center;
align-items: flex-start;
width: 100%;
padding-top: 50px;
box-sizing: border-box;
}
.image-wrapper {
background-color: #233B53;
width: 950px;
min-height: 555px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
position: relative;
border-radius: 5px;
overflow: hidden;
flex-wrap: wrap;
height: auto;
}
.image-container {
position: relative;
width: 635px;
height: 360px;
padding-top: 15px;
padding-left: 15px;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: contain;
}
.details-container {
width: 635px;
height: auto;
background-color: #19222C;
border: 3px solid #35465E;
box-sizing: border-box;
margin-top: 20px;
display: flex;
flex-direction: column;
justify-content: flex-start;
padding: 10px 15px 10px 15px;
margin-left: 15px;
position: relative;
}
.card-title {
font-size: 20px;
font-family: Arial, sans-serif;
font-weight: bold;
margin: 0;
}
.description {
font-size: 16px;
font-family: Arial, sans-serif;
color: #A1B0C7;
margin-top: 10px;
line-height: 1.5;
}
.download-btn {
width: 138px;
height: 35px;
background: linear-gradient(to bottom, #A4D007, #536904);
color: white;
border: none;
border-radius: 2px;
font-family: Arial, sans-serif;
font-size: 19px;
cursor: pointer;
position: absolute;
top: 10px;
right: 15px;
}
.download-btn:hover {
background: linear-gradient(to bottom, #8DC50E, #475F2D);
}
.game-mode {
position: absolute;
top: 15px;
left: 660px;
font-size: 13px;
color: #A1B0C7;
font-family: Arial, sans-serif;
font-weight: normal;
padding-left: 5px;
}
.dynamic-data {
font-size: 14px;
font-weight: normal;
color: white;
}
.tags {
position: absolute;
top: 100px;
left: 660px;
font-size: 13px;
color: #A1B0C7;
font-family: Arial, sans-serif;
font-weight: normal;
padding-left: 5px;
}
.file-size {
position: absolute;
top: 150px;
left: 660px;
font-size: 13px;
color: #A1B0C7;
font-family: Arial, sans-serif;
font-weight: normal;
padding-left: 5px;
}
.file-size .dynamic-data {
font-size: 13px;
font-weight: normal;
color: #A1B0C7;
margin-left: 16px;
}
.added-time {
position: absolute;
top: 170px;
left: 660px;
font-size: 13px;
color: #A1B0C7;
font-family: Arial, sans-serif;
font-weight: normal;
padding-left: 5px;
}
.added-time .dynamic-data {
font-size: 13px;
font-weight: normal;
color: #A1B0C7;
margin-left: 45px;
}
.youtube-container {
margin-top: 15px;
width: 265px;
height: 150px;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 210px;
left: 660px;
}

64
frontend/main.html Normal file
View file

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workshop</title>
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
</head>
<body>
<div class="main-container">
<div class="image-wrapper">
<div class="image-container">
<img src="{{ image_url }}" alt="Map Image">
</div>
<div class="details-container">
<div class="card-title">{{ map_title }}</div>
<div class="description">
{{ description }}
</div>
<a href="{{ url_for('download_bsp', image_path=image_url) }}">
<button class="download-btn">🡇 Скачать</button>
</a>
</div>
<div class="game-mode">
<span>Режим игры:</span>
<span class="dynamic-data">{{ game_mode | default('Не указан') }}</span>
</div>
<div class="tags">
<span>Метки:</span>
<span class="dynamic-data">{{ tags | default('Не указаны') }}</span>
</div>
<div class="file-size">
<span>Размер файла:</span>
<span class="dynamic-data">{{ file_size | default('Не указан') }}</span>
</div>
<div class="added-time">
<span>Добавлен:</span>
<span class="dynamic-data">{{ added_time | default('Не указано') }}</span>
</div>
{% if youtube_link %}
<div class="youtube-container">
<iframe
width="265"
height="150"
src="{{ youtube_link | replace('watch?v=', 'embed/') }}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
{% endif %}
</div>
</div>
</body>
</html>

8
frontend/main.js Normal file
View file

@ -0,0 +1,8 @@
document.addEventListener('DOMContentLoaded', () => {
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.addEventListener('click', () => {
window.location.href = '/main';
});
});
});

487
frontend/workshop.css Normal file
View file

@ -0,0 +1,487 @@
body {
background-color: #1B2A3C;
color: white;
margin: 0;
padding: 0;
display: block;
}
.top-bar {
background-color: #171D25;
width: 100%;
height: 105px;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
z-index: 1000;
}
.main-container {
display: flex;
justify-content: center;
align-items: flex-start;
margin-top: 105px;
width: 100%;
position: relative;
flex-wrap: wrap;
}
.cards-container {
display: grid;
grid-template-columns: repeat(3, 200px);
grid-gap: 105px 20px;
justify-content: center;
margin-bottom: 40px;
}
.right-rectangle {
width: 295px;
height: 370px;
background: linear-gradient(to left, #0E141C, #111A23, #15202C);
margin-left: 20px;
margin-top: 40px;
border-radius: 0;
position: relative;
}
.show-products-title {
font-family: Arial, sans-serif;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 5px;
margin-left: 5px;
}
.game-modes-title {
font-family: Arial, sans-serif;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 0px;
margin-left: 5px;
}
.game-modes {
display: flex;
flex-direction: column;
gap: 5px;
position: absolute;
bottom: 10px;
left: 10px;
}
.game-mode {
font-family: Arial, sans-serif;
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
display: flex;
align-items: center;
position: relative;
}
.game-mode input[type="checkbox"] {
margin-right: 10px;
width: 16px;
height: 16px;
cursor: pointer;
accent-color: blue;
}
.game-mode:hover {
background-color: #1B2A3C;
}
.game-mode:hover::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 295px;
height: 10px;
background-color: #1B2A3C;
z-index: -1;
}
.card {
position: relative;
width: 200px;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.stars {
width: 81px;
height: 14px;
margin: 5px 0;
}
.card-title {
font-size: 16px;
color: white;
margin-top: 5px;
text-align: left;
}
.card img:not(.stars) {
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
}
.description-popup {
display: none;
position: absolute;
width: 300px;
background-color: #61ABD7;
color: white;
font-size: 14px;
padding: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
z-index: 10;
left: 220px;
top: 0;
white-space: normal;
word-wrap: break-word;
}
.description-popup::after {
content: "";
position: absolute;
top: 10px;
left: -20px;
border-width: 10px;
border-style: solid;
border-color: transparent #61ABD7 transparent transparent;
}
.description-popup strong {
font-size: 16px;
margin-bottom: 10px;
display: block;
}
.description-popup p {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}
.card:hover .description-popup {
display: block;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
position: relative;
bottom: 25px;
left: 0px;
width: 100%;
}
.pagebtn {
width: 40px;
height: 16px;
background-color: #2B475E;
color: #63AFCD;
font-family: Arial, sans-serif;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
margin: 0 10px;
border-radius: 2px;
transition: background-color 0.3s, color 0.3s;
border: none;
}
.pagebtn:hover {
background-color: #66C0F4;
color: white;
}
.pagelink {
background-color: transparent;
color: white;
font-family: Arial, sans-serif;
font-size: 16px;
text-decoration: none;
margin: 0 5px;
}
.pagination_space {
margin: 0 5px;
}
.pagebtn:hover, .pagelink:hover {
background-color: #2A5B70;
}
.sort-button-container {
display: flex;
justify-content: flex-start;
margin-top: 15px;
margin-left: 15px;
}
.sort-button {
width: 148px;
height: 30px;
background: linear-gradient(to top, #384A65, #57749E);
color: white;
font-family: Arial, sans-serif;
font-size: 13px;
font-weight: normal;
border: none;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
margin-top: 0px;
}
.sort-button:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.sort-button:active {
background: linear-gradient(to top, #2C3E55, #3D5B80);
}
.modal {
display: none;
position: fixed;
z-index: 1001;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 530px;
height: 315px;
background: linear-gradient(to left, #333840, #333840);
padding: 20px;
}
.modal-content {
color: white;
font-family: Arial, sans-serif;
position: relative;
}
.close-btn {
position: absolute;
top: -40px;
right: -15px;
color: white;
font-size: 30px;
cursor: pointer;
opacity: 0.7;
}
.close-btn:hover {
color: #f1f1f1;
opacity: 1;
}
.modal-content h2 {
margin-left: 25px;
}
.modal-rectangle {
position: absolute;
top: 105px;
left: 25px;
width: 480px;
height: 42px;
background-color: #292B2F;
border-radius: 3px;
z-index: 1002;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
}
.modal-rectangle .text-left {
font-family: Arial, sans-serif;
font-weight: bold;
font-size: 15px;
}
.modal-rectangle .text-center {
left: 240px;
font-family: Arial, sans-serif;
font-weight: bold;
font-size: 15px;
}
.date-input {
background-color: #33363E;
color: white;
border: none;
font-size: 14px;
height: 30px;
width: 205px;
border-radius: 3px;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
z-index: 1003;
}
.date-input::-webkit-calendar-picker-indicator {
display: none;
}
.date-input:focus {
outline: none;
background-color: #4c4f58;
}
.cancel-button {
position: absolute;
bottom: -270px;
right: 25px;
width: 80px;
height: 30px;
background-color: #32363F;
color: white;
font-size: 14px;
font-family: Arial, sans-serif;
border: none;
border-radius: 3px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2);
cursor: pointer;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
.cancel-button:hover {
background-color: #4A4F5A;
box-shadow: 0px 6px 8px rgba(0, 0, 0, 0.3);
}
.ok-button {
position: absolute;
bottom: -270px;
right: 130px;
width: 50px;
height: 30px;
background-color: #6EA720;
color: white;
font-size: 14px;
font-family: Arial, sans-serif;
border: none;
border-radius: 3px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2);
cursor: pointer;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
.ok-button:hover {
background-color: #A6FC30;
box-shadow: 0px 6px 8px rgba(0, 0, 0, 0.3);
}
.search-container {
margin-top: 420px;
display: flex;
justify-content: center;
}
.search-input-container {
position: relative;
}
.search-input {
width: 250px;
height: 40px;
background-color: #2C3E55;
color: white;
font-size: 14px;
border: none;
border-radius: 3px;
padding-left: 10px;
outline: none;
padding-right: 40px;
}
.search-input::placeholder {
color: rgba(255, 255, 255, 0.7);
}
.search-input:focus {
background-color: #3E4A61;
}
.search-button {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.search-button img {
width: 25px;
height: 25px;
object-fit: contain;
}
.filter-stars {
margin-top: 420px;
margin-right: 260px;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
.filter-title {
margin-bottom: 10px;
}
.filter-stars {
position: absolute;
margin-top: 420px;
margin-right: -660px;
display: flex;
flex-direction: column;
align-items: center;
background-color: #2C3E55;
width: 300px;
height: 80px;
border-radius: 3px;
padding: 10px;
box-sizing: border-box;
}
.filter-title {
margin-bottom: 10px;
color: white;
font-size: 16px;
}
.stars-filter {
display: flex;
justify-content: space-between;
width: 100%;
}
.stars-filter label {
font-family: Arial, sans-serif;
font-size: 14px;
color: white;
display: flex;
flex-direction: column-reverse;
align-items: center;
}
.stars-filter input[type="checkbox"] {
margin-bottom: 5px;
width: 16px;
height: 16px;
cursor: pointer;
accent-color: #63AFCD;
}

138
frontend/workshop.html Normal file
View file

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workshop</title>
<link rel="stylesheet" href="{{ url_for('static', filename='workshop.css') }}">
<script src="{{ url_for('static', filename='workshop.js') }}" defer></script>
</head>
<body>
<div class="top-bar">
<h1>Добро пожаловать в Workshop</h1>
</div>
<div class="main-container">
<div class="cards-container">
{% for map in maps_data %}
<div class="card">
<a href="/main?image_url={{ get_image_path(map[0]) }}&map_title={{ map[1] }}&{{ filters }}">
<img src="{{ get_image_path(map[0]) }}" alt="Map Image" width="200" height="110">
</a>
<img src="{{ get_star_image(map[2]) }}" alt="{{ map[2] }} stars" class="stars" width="81" height="14">
<div class="card-title">{{ map[1] }}</div>
<div class="description-popup">
<strong>{{ map[1] }}</strong>
<p>{{ map[3] }}</p>
</div>
</div>
{% endfor %}
</div>
<div class="right-rectangle">
<div class="sort-button-container">
<button class="sort-button">Сортировать по дате</button>
</div>
<div class="game-modes">
<div class="show-products-title">
Показать продукты, попадающие в каждую из выбранных категорий:
</div>
<div class="game-modes-title">РЕЖИМ ИГРЫ</div>
{% for mode, label in {
"Classic": "Классический",
"Deathmatch": "Бой насмерть",
"Demolition": "Уничтожение объекта",
"Armsrace": "Гонка вооружений",
"Custom": "Пользовательский",
"Training": "Обучение",
"Co-op Strike": "Совместный налёт",
"Wingman": "Напарники",
"Flying Scoutsman": "Перелётные снайперы"
}.items() %}
<label class="game-mode">
<input type="checkbox" class="game-mode-checkbox" value="{{ mode }}" {% if mode in filters %}checked{% endif %}>
{{ label }}
</label>
{% endfor %}
</div>
<div class="search-container">
<form method="get" action="/">
<div class="search-input-container">
<input type="text" id="search" class="search-input" name="search_title" placeholder="Поиск по названию" value="{{ request.args.get('search_title', '') }}">
<button class="search-button" type="submit">
<img src="/images/search-icon.png" alt="Поиск">
</button>
</div>
</form>
</div>
</div>
<div id="sortModal" class="modal">
<div class="modal-content">
<span class="close-btn">&times;</span>
<h2>Сортировать по дате</h2>
<div class="modal-rectangle">
<span class="text-left">С</span>
<div class="first-rectangle">
<input type="date" class="date-input" />
</div>
<span class="text-center">ПО</span>
<div class="second-rectangle">
<input type="date" class="date-input" />
</div>
</div>
<button class="ok-button">ОК</button>
<button class="cancel-button">Отмена</button>
</div>
</div>
<div class="filter-stars">
<div class="filter-title">
<label>Фильтр по количеству звезд:</label>
</div>
<div class="stars-filter">
<label><input type="checkbox" name="stars" value="1" {% if 'stars' in request.args and '1' in request.args.getlist('stars') %}checked{% endif %}> 1</label>
<label><input type="checkbox" name="stars" value="2" {% if 'stars' in request.args and '2' in request.args.getlist('stars') %}checked{% endif %}> 2</label>
<label><input type="checkbox" name="stars" value="3" {% if 'stars' in request.args and '3' in request.args.getlist('stars') %}checked{% endif %}> 3</label>
<label><input type="checkbox" name="stars" value="4" {% if 'stars' in request.args and '4' in request.args.getlist('stars') %}checked{% endif %}> 4</label>
<label><input type="checkbox" name="stars" value="5" {% if 'stars' in request.args and '5' in request.args.getlist('stars') %}checked{% endif %}> 5</label>
</div>
</div>
<div class="pagination">
{% if page > 1 %}
<a class="pagebtn" href="/?page={{ page - 1 }}&{{ filters }}">&#60;</a>
{% endif %}
{% if page > 3 %}
<a class="pagelink" href="/?page=1&{{ filters }}">1</a>
<span class="pagination_space">...</span>
{% endif %}
{% for p in range(1, total_pages + 1) %}
{% if p >= page - 1 and p <= page + 2 %}
{% if p == page %}
<span class="pagelink" style="color: #417A9B;">{{ p }}</span>
{% else %}
<a class="pagelink" href="/?page={{ p }}&{{ filters }}">{{ p }}</a>
{% endif %}
{% endif %}
{% endfor %}
{% if total_pages > 3 and page < total_pages - 2 %}
<span class="pagination_space">...</span>
<a class="pagelink" href="/?page={{ total_pages }}&{{ filters }}">{{ total_pages }}</a>
{% endif %}
{% if page < total_pages %}
<a class="pagebtn" href="/?page={{ page + 1 }}&{{ filters }}">&#62;</a>
{% endif %}
</div>
</div>
</body>
</html>

144
frontend/workshop.js Normal file
View file

@ -0,0 +1,144 @@
document.addEventListener('DOMContentLoaded', function () {
const sortButton = document.querySelector('.sort-button');
const modal = document.getElementById('sortModal');
const closeButton = document.querySelector('.close-btn');
const cancelButton = document.querySelector('.cancel-button');
const okButton = document.querySelector('.ok-button');
const startDateInput = document.querySelector('.first-rectangle .date-input');
const endDateInput = document.querySelector('.second-rectangle .date-input');
const checkboxes = document.querySelectorAll('.game-mode-checkbox');
const params = new URLSearchParams(window.location.search);
// Устанавливаем состояние чекбоксов при загрузке страницы
checkboxes.forEach(checkbox => {
checkbox.checked = params.getAll('game_modes').includes(checkbox.value);
});
// Открыть модальное окно при нажатии на кнопку
sortButton.addEventListener('click', function () {
modal.style.display = 'block';
});
// Закрыть модальное окно при нажатии на крестик
closeButton.addEventListener('click', function () {
modal.style.display = 'none';
});
// Закрыть модальное окно при нажатии на кнопку "Отмена"
cancelButton.addEventListener('click', function () {
modal.style.display = 'none';
});
// Применить фильтры по дате и режимам игры при нажатии на кнопку "OK"
okButton.addEventListener('click', function () {
const startDate = startDateInput.value;
const endDate = endDateInput.value;
// Фильтры по датам
if (startDate) params.set('start_date', startDate);
else params.delete('start_date');
if (endDate) params.set('end_date', endDate);
else params.delete('end_date');
// Фильтры по режимам игры
const selectedGameModes = Array.from(checkboxes)
.filter(checkbox => checkbox.checked)
.map(checkbox => checkbox.value);
// Обновляем параметры для выбранных режимов
params.delete('game_modes'); // Удаляем старые значения
selectedGameModes.forEach(mode => params.append('game_modes', mode));
params.set('page', 1); // Сбрасываем на первую страницу
modal.style.display = 'none'; // Закрываем модальное окно
window.location.search = params.toString(); // Перезагружаем страницу с новыми параметрами
});
// Обновление параметров при изменении чекбоксов
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function () {
const selectedGameModes = Array.from(checkboxes)
.filter(checkbox => checkbox.checked)
.map(checkbox => checkbox.value);
// Обновляем параметры URL
params.delete('game_modes'); // Удаляем старые значения
selectedGameModes.forEach(mode => params.append('game_modes', mode));
params.set('page', 1); // Сбрасываем на первую страницу
window.location.search = params.toString(); // Перезагружаем страницу с новыми параметрами
});
});
});
document.addEventListener('DOMContentLoaded', function () {
const searchInput = document.querySelector('.search-input');
// Функция для обработки поиска
function performSearch() {
const query = searchInput.value.toLowerCase();
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
const title = card.querySelector('.card-title').textContent.toLowerCase();
if (title.includes(query)) {
card.style.display = 'block'; // Показываем карточку, если название соответствует запросу
} else {
card.style.display = 'none'; // Скрываем карточку, если название не соответствует запросу
}
});
}
// Поиск по нажатию клавиши Enter
searchInput.addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
performSearch();
}
});
// Поиск по клику на лупу (если такая кнопка добавлена)
const searchButton = document.querySelector('.search-button');
if (searchButton) {
searchButton.addEventListener('click', performSearch);
}
});
document.addEventListener('DOMContentLoaded', function () {
const starsCheckboxes = document.querySelectorAll('input[name="stars"]');
const params = new URLSearchParams(window.location.search);
// Устанавливаем состояние чекбоксов при загрузке страницы
const selectedStars = params.get('stars');
if (selectedStars) {
starsCheckboxes.forEach(checkbox => {
if (checkbox.value === selectedStars) {
checkbox.checked = true;
}
});
}
// Обновление параметров при изменении чекбоксов для звезд
starsCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function () {
// Снимаем отметки с других чекбоксов
starsCheckboxes.forEach(otherCheckbox => {
if (otherCheckbox !== checkbox) {
otherCheckbox.checked = false;
}
});
const selectedStar = checkbox.checked ? checkbox.value : null;
// Обновляем параметры URL
params.delete('stars');
if (selectedStar) {
params.set('stars', selectedStar); // Устанавливаем выбранную звезду
}
params.set('page', 1); // Сбрасываем на первую страницу
window.location.search = params.toString(); // Перезагружаем страницу с новыми параметрами
});
});
});