Decider admin
This commit is contained in:
+652
-2
@@ -1,6 +1,656 @@
|
||||
@()(implicit r: Request[_])
|
||||
@(deciders: Seq[repository.Decider])(implicit request: Request[_], messages: Messages)
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="@{play.filters.csrf.CSRF.getToken.map(_.value).getOrElse("")}">
|
||||
<title>Admin — Decider</title>
|
||||
<link rel='stylesheet' href='@routes.Assets.at("lib/bootstrap/css/bootstrap.min.css")'>
|
||||
<style>
|
||||
body { padding-top: 70px; }
|
||||
.decider-card { margin-bottom: 15px; border-left: 4px solid #337ab7; }
|
||||
/* Строка карты занимает всю доступную ширину: выбор карты фиксированный, описание растягивается */
|
||||
.map-row { margin-bottom: 5px; display: flex; align-items: center; }
|
||||
.map-row .map-desc { flex: 1 1 auto; min-width: 0; width: auto; }
|
||||
.map-row .map-preview, .map-row .btn-remove-map { flex: 0 0 auto; }
|
||||
.modal-footer { text-align: right; }
|
||||
.btn-remove-map { margin-left: 5px; }
|
||||
.decider-key { font-family: monospace; color: #666; font-size: 0.9em; }
|
||||
.nav-tabs > li > a { cursor: pointer; }
|
||||
#deciderList { max-height: calc(100vh - 200px); overflow-y: auto; }
|
||||
#deciderList .list-group-item { cursor: pointer; }
|
||||
#deciderList .list-group-item.active { background-color: #337ab7; border-color: #337ab7; }
|
||||
#deciderList .list-group-item:hover { background-color: #f5f5f5; }
|
||||
#deciderList .list-group-item.active:hover { background-color: #286090; }
|
||||
.error-msg { color: red; margin-top: 10px; }
|
||||
.success-msg { color: green; margin-top: 10px; }
|
||||
.map-preview { width: 64px; height: 40px; object-fit: cover; border-radius: 3px; margin-right: 6px; vertical-align: middle; }
|
||||
/* Кнопка выбора карты вместо ручного ввода имени */
|
||||
.btn-select-map { width: 170px; flex: 0 0 170px; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 6px; }
|
||||
.map-gallery { max-height: 60vh; overflow-y: auto; }
|
||||
/* Поиск карт и загрузка картинки */
|
||||
.map-picker-toolbar { display: flex; align-items: center; margin-bottom: 10px; }
|
||||
.map-picker-toolbar .map-search { flex: 1 1 auto; margin-right: 8px; }
|
||||
#mapUploadMessage { min-height: 18px; margin-bottom: 8px; }
|
||||
.map-gallery .map-thumb { cursor: pointer; text-align: center; margin-bottom: 10px; padding: 4px; border: 1px solid transparent; border-radius: 4px; }
|
||||
.map-gallery .map-thumb:hover { border-color: #337ab7; background-color: #f5f5f5; }
|
||||
.map-gallery .map-thumb img { width: 100%; max-width: 160px; height: 100px; object-fit: cover; border-radius: 4px; }
|
||||
.map-gallery .map-thumb-name { font-size: 11px; word-break: break-all; margin-top: 3px; }
|
||||
#mapPickerTabs { margin-bottom: 15px; }
|
||||
|
||||
<html>
|
||||
/* Шапка: название слева, Steam ID и Refresh справа, всё по центру по вертикали */
|
||||
.navbar .container { display: flex; align-items: center; justify-content: space-between; }
|
||||
.navbar-header { float: none; }
|
||||
.navbar-user { display: flex; align-items: center; margin-left: 15px; color: #ccc; }
|
||||
.navbar-user .navbar-user-id { margin-right: 10px; white-space: nowrap; }
|
||||
.navbar-user .btn { margin: 0; }
|
||||
|
||||
/* Кнопки в заголовках панелей по центру строки с заголовком */
|
||||
.panel-heading .panel-title { display: flex; align-items: center; justify-content: space-between; }
|
||||
.panel-heading .panel-title > .btn { flex: 0 0 auto; margin-left: 10px; }
|
||||
|
||||
@@media (max-width: 480px) {
|
||||
.navbar-user .navbar-user-id { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/decider/admin">Decider admin</a>
|
||||
</div>
|
||||
<div class="navbar-user">
|
||||
<span class="navbar-user-id">Steam ID: @(request.session.get("steamId").getOrElse("unknown"))</span>
|
||||
<a href="/decider/admin" class="btn btn-sm btn-default">Refresh</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<!-- Left panel: list of deciders -->
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Deciders (@deciders.size)
|
||||
<button class="btn btn-sm btn-success" onclick="openNewDecider()">+ New</button>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="list-group" id="deciderList">
|
||||
@for(d <- deciders) {
|
||||
<a class="list-group-item" onclick="openDecider('@d.key')">
|
||||
<h4 class="list-group-item-heading">@d.name</h4>
|
||||
<p class="list-group-item-text decider-key">@d.key • maps: @d.maps.size</p>
|
||||
</a>
|
||||
}
|
||||
@if(deciders.isEmpty) {
|
||||
<div class="list-group-item text-muted">No deciders</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right panel: edit form -->
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default" id="editPanel" style="display: none;">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<span id="editTitle">Edit</span>
|
||||
<button class="btn btn-sm btn-danger" id="btnDelete" onclick="deleteCurrentDecider()">Delete</button>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form id="deciderForm">
|
||||
<input type="hidden" id="deciderKey">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="formKey">Key</label>
|
||||
<input type="text" class="form-control" id="formKey" placeholder="e.g. dawn_of_war">
|
||||
<p class="help-block">Unique decider identifier (letters, digits, hyphen, underscore)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="formName">Name</label>
|
||||
<input type="text" class="form-control" id="formName" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="formRaceCount">Race Count</label>
|
||||
<input type="number" class="form-control" id="formRaceCount" min="0">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="formRules">Rules</label>
|
||||
<textarea class="form-control" id="formRules" rows="5"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="formExistAdditionalRaces"> Additional Races (Race count per team doubled)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="formExistTextFieldRaces"> Exist Text Field Races (for Unification like mods)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="formDescription">Map description
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Maps</h4>
|
||||
<div id="mapsContainer"></div>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="openMapPicker()">+ Add map</button>
|
||||
|
||||
<hr>
|
||||
<div class="form-group text-right">
|
||||
<button type="button" class="btn btn-default" onclick="closeEditPanel()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveDecider()">Save</button>
|
||||
</div>
|
||||
<div id="saveMessage"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="text-muted text-center" style="padding: 50px;">
|
||||
<h3>Select a decider to edit</h3>
|
||||
<p>or create a new one</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirmation modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Confirm deletion</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete decider <strong id="deleteDeciderName"></strong>?</p>
|
||||
<p class="text-danger">This action cannot be undone!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map image picker modal -->
|
||||
<div class="modal fade" id="mapPickerModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Choose a map</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="map-picker-toolbar">
|
||||
<input type="search" id="mapSearch" class="form-control map-search" placeholder="Search maps..." autocomplete="off">
|
||||
<button type="button" class="btn btn-sm btn-primary" id="btnUploadMap">Upload image</button>
|
||||
<input type="file" id="mapUploadInput" accept="image/jpeg,image/png,image/webp" style="display: none;">
|
||||
</div>
|
||||
<div id="mapUploadMessage"></div>
|
||||
<ul class="nav nav-pills" id="mapPickerTabs"></ul>
|
||||
<div class="row map-gallery" id="mapGallery"></div>
|
||||
<div id="mapPickerEmpty" class="text-muted" style="display: none;">No map images found</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src='@routes.Assets.at("lib/jquery/jquery.min.js")'></script>
|
||||
<script src='@routes.Assets.at("lib/bootstrap/js/bootstrap.min.js")'></script>
|
||||
<script src='@routes.Assets.at("lib/underscore/underscore.js")'></script>
|
||||
<script>
|
||||
var currentDeciderKey = null;
|
||||
|
||||
// CSRF: все AJAX-запросы отправляем с токеном из meta-тега,
|
||||
// иначе Play отклоняет POST/DELETE с JSON-телом.
|
||||
(function() {
|
||||
var token = $('meta[name="csrf-token"]').attr('content');
|
||||
if (token) {
|
||||
$.ajaxSetup({ headers: { 'Csrf-Token': token } });
|
||||
}
|
||||
})();
|
||||
|
||||
// Open decider for editing
|
||||
function openDecider(key) {
|
||||
currentDeciderKey = key;
|
||||
$.ajax({
|
||||
url: '/decider/admin/deciders/' + encodeURIComponent(key),
|
||||
method: 'GET',
|
||||
success: function(decider) {
|
||||
showEditPanel(decider);
|
||||
},
|
||||
error: function() {
|
||||
alert('Failed to load decider');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Open new decider form
|
||||
function openNewDecider() {
|
||||
currentDeciderKey = null;
|
||||
showEditPanel({
|
||||
key: '',
|
||||
name: '',
|
||||
rules: '',
|
||||
raceCount: 1,
|
||||
existAdditionalRaces: false,
|
||||
existTextFieldRaces: false,
|
||||
description: false,
|
||||
maps: []
|
||||
});
|
||||
}
|
||||
|
||||
// Show edit panel with decider data
|
||||
function showEditPanel(decider) {
|
||||
$('#emptyState').hide();
|
||||
$('#editPanel').show();
|
||||
$('#editTitle').text(decider.key ? 'Edit: ' + decider.name : 'New decider');
|
||||
|
||||
$('#deciderKey').val(decider.key);
|
||||
$('#formKey').val(decider.key).prop('readonly', !!decider.key);
|
||||
$('#btnDelete').toggle(!!decider.key);
|
||||
$('#formName').val(decider.name);
|
||||
$('#formRules').val(decider.rules);
|
||||
$('#formRaceCount').val(decider.raceCount);
|
||||
$('#formExistAdditionalRaces').prop('checked', decider.existAdditionalRaces);
|
||||
$('#formExistTextFieldRaces').prop('checked', decider.existTextFieldRaces);
|
||||
$('#formDescription').prop('checked', decider.description);
|
||||
|
||||
// Render maps
|
||||
var container = $('#mapsContainer');
|
||||
container.empty();
|
||||
if (decider.maps && decider.maps.length > 0) {
|
||||
decider.maps.forEach(function(map, i) {
|
||||
addMapRow(map.map, map.description);
|
||||
});
|
||||
} else {
|
||||
addMapRow();
|
||||
}
|
||||
|
||||
// Превью карт: подгружаем список картинок, чтобы найти файлы любого формата.
|
||||
ensureMapImages();
|
||||
|
||||
// Highlight selected in list
|
||||
$('.list-group-item').removeClass('active');
|
||||
$('.list-group-item').filter(function() {
|
||||
return $(this).find('.decider-key').text().includes(decider.key);
|
||||
}).addClass('active');
|
||||
}
|
||||
|
||||
// Close edit panel
|
||||
function closeEditPanel() {
|
||||
$('#editPanel').hide();
|
||||
$('#emptyState').show();
|
||||
currentDeciderKey = null;
|
||||
}
|
||||
|
||||
// ---------- Map image picker ----------
|
||||
|
||||
var mapImages = null;
|
||||
var pickerTargetRow = null;
|
||||
var activeMapGroup = 2;
|
||||
var mapSearchQuery = '';
|
||||
|
||||
// Группы карт: 1x1, 2x2, 3x3, 4x4 и остальные.
|
||||
var mapGroups = [
|
||||
{ players: 2, title: '1x1 (2p)' },
|
||||
{ players: 4, title: '2x2 (4p)' },
|
||||
{ players: 6, title: '3x3 (6p)' },
|
||||
{ players: 8, title: '4x4 (8p)' },
|
||||
{ players: 0, title: 'Other' }
|
||||
];
|
||||
|
||||
function openMapPicker(targetRow) {
|
||||
pickerTargetRow = targetRow || null;
|
||||
mapSearchQuery = '';
|
||||
$('#mapSearch').val('');
|
||||
$('#mapUploadMessage').text('');
|
||||
if (mapImages) {
|
||||
showMapPicker();
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: '/decider/admin/maps',
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
mapImages = data;
|
||||
showMapPicker();
|
||||
},
|
||||
error: function() {
|
||||
alert('Failed to load map images');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showMapPicker() {
|
||||
// Если в текущей группе нет карт, открываем первую непустую.
|
||||
if (mapsInGroup(activeMapGroup).length === 0) {
|
||||
var firstNonEmpty = _.find(mapGroups, function(g) {
|
||||
return mapsInGroup(g.players).length > 0;
|
||||
});
|
||||
activeMapGroup = firstNonEmpty ? firstNonEmpty.players : 2;
|
||||
}
|
||||
renderMapTabs();
|
||||
renderMapGallery();
|
||||
$('#mapPickerModal').modal('show');
|
||||
}
|
||||
|
||||
function mapsInGroup(players) {
|
||||
return (mapImages || []).filter(function(m) {
|
||||
return players === 0
|
||||
? [2, 4, 6, 8].indexOf(m.players) === -1
|
||||
: m.players === players;
|
||||
});
|
||||
}
|
||||
|
||||
// Карты для галереи: при активном поиске ищем по всем группам сразу.
|
||||
function visibleMaps() {
|
||||
if (!mapSearchQuery) {
|
||||
return mapsInGroup(activeMapGroup);
|
||||
}
|
||||
return (mapImages || []).filter(function(m) {
|
||||
return (m.name || '').toLowerCase().indexOf(mapSearchQuery) !== -1 ||
|
||||
(m.file || '').toLowerCase().indexOf(mapSearchQuery) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
function renderMapTabs() {
|
||||
var tabs = $('#mapPickerTabs').empty();
|
||||
// Во время поиска вкладки групп не нужны: показываются все совпадения.
|
||||
if (mapSearchQuery) {
|
||||
tabs.hide();
|
||||
return;
|
||||
}
|
||||
tabs.show();
|
||||
mapGroups.forEach(function(group) {
|
||||
var count = mapsInGroup(group.players).length;
|
||||
if (group.players === 0 && count === 0) return;
|
||||
var link = $('<a href="#">')
|
||||
.text(group.title + ' (' + count + ')')
|
||||
.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
activeMapGroup = group.players;
|
||||
renderMapTabs();
|
||||
renderMapGallery();
|
||||
});
|
||||
tabs.append($('<li>').toggleClass('active', group.players === activeMapGroup).append(link));
|
||||
});
|
||||
}
|
||||
|
||||
function renderMapGallery() {
|
||||
var gallery = $('#mapGallery').empty();
|
||||
var maps = visibleMaps();
|
||||
maps.forEach(function(map) {
|
||||
var thumb = $('<div class="map-thumb">')
|
||||
.append($('<img>').attr('src', mapImageUrl(map.file)).attr('alt', map.name).attr('title', map.name))
|
||||
.append($('<div class="map-thumb-name">').text(map.name))
|
||||
.on('click', function() {
|
||||
chooseMap(map.name);
|
||||
});
|
||||
gallery.append($('<div class="col-xs-6 col-sm-4 col-md-3">').append(thumb));
|
||||
});
|
||||
$('#mapPickerEmpty')
|
||||
.text(mapSearchQuery ? 'No maps match your search' : 'No map images found')
|
||||
.toggle(maps.length === 0);
|
||||
}
|
||||
|
||||
// Перечитывает список картинок с сервера и перерисовывает галерею.
|
||||
function reloadMapImages(preferredGroup) {
|
||||
$.ajax({
|
||||
url: '/decider/admin/maps',
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
mapImages = data;
|
||||
if (preferredGroup !== undefined) {
|
||||
activeMapGroup = preferredGroup;
|
||||
}
|
||||
renderMapTabs();
|
||||
renderMapGallery();
|
||||
},
|
||||
error: function() {
|
||||
alert('Failed to load map images');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Поиск и загрузка картинок карт ----------
|
||||
|
||||
$('#mapSearch').on('input', function() {
|
||||
mapSearchQuery = $(this).val().trim().toLowerCase();
|
||||
renderMapTabs();
|
||||
renderMapGallery();
|
||||
});
|
||||
|
||||
$('#btnUploadMap').on('click', function() {
|
||||
$('#mapUploadInput').click();
|
||||
});
|
||||
|
||||
$('#mapUploadInput').on('change', function() {
|
||||
var input = this;
|
||||
var file = input.files && input.files[0];
|
||||
if (!file) return;
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
var msgEl = $('#mapUploadMessage');
|
||||
msgEl.removeClass('text-danger').addClass('text-muted').text('Uploading...');
|
||||
|
||||
$.ajax({
|
||||
url: '/decider/admin/maps',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(image) {
|
||||
msgEl.removeClass('text-danger').text('');
|
||||
input.value = '';
|
||||
mapSearchQuery = '';
|
||||
$('#mapSearch').val('');
|
||||
reloadMapImages(image ? image.players : undefined);
|
||||
},
|
||||
error: function(xhr) {
|
||||
var error = 'Upload failed';
|
||||
if (xhr.responseJSON && xhr.responseJSON.error) {
|
||||
error = xhr.responseJSON.error;
|
||||
}
|
||||
msgEl.removeClass('text-muted').addClass('text-danger').text(error);
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function chooseMap(mapName) {
|
||||
if (pickerTargetRow && $.contains(document, pickerTargetRow[0])) {
|
||||
pickerTargetRow.find('.map-name').val(mapName);
|
||||
updateMapRow(pickerTargetRow);
|
||||
} else {
|
||||
addMapRow(mapName, '');
|
||||
}
|
||||
pickerTargetRow = null;
|
||||
$('#mapPickerModal').modal('hide');
|
||||
}
|
||||
|
||||
function mapImageUrl(file) {
|
||||
return '/assets/images/maps/' + encodeURIComponent(file);
|
||||
}
|
||||
|
||||
// ---------- Map rows ----------
|
||||
|
||||
// Add map row
|
||||
function addMapRow(mapName, description) {
|
||||
var row = $('<div class="map-row form-inline">');
|
||||
|
||||
var preview = $('<img class="map-preview" alt="">');
|
||||
// Имя карты задаётся только через выбор из галереи, ручной ввод запрещён.
|
||||
var nameInput = $('<input type="hidden" class="map-name">')
|
||||
.val(mapName || '');
|
||||
var descInput = $('<input type="text" class="form-control map-desc" placeholder="Description">')
|
||||
.val(description || '');
|
||||
var selectBtn = $('<button type="button" class="btn btn-sm btn-default btn-select-map">Select map</button>')
|
||||
.on('click', function() {
|
||||
openMapPicker(row);
|
||||
});
|
||||
var removeBtn = $('<button type="button" class="btn btn-sm btn-danger btn-remove-map" title="Remove map">✕</button>')
|
||||
.on('click', function() {
|
||||
row.remove();
|
||||
});
|
||||
|
||||
row.append(preview, nameInput, selectBtn, descInput, removeBtn);
|
||||
$('#mapsContainer').append(row);
|
||||
updateMapRow(row);
|
||||
}
|
||||
|
||||
// Обновляет превью и подпись кнопки выбора карты
|
||||
function updateMapRow(row) {
|
||||
var mapName = row.find('.map-name').val().trim();
|
||||
var selectBtn = row.find('.btn-select-map');
|
||||
selectBtn.text(mapName || 'Select map').attr('title', mapName || 'Select map');
|
||||
|
||||
var img = row.find('.map-preview').off('error').off('load');
|
||||
if (!mapName) {
|
||||
img.hide().removeAttr('src');
|
||||
return;
|
||||
}
|
||||
img.attr('src', mapImageUrl(mapImageFileFor(mapName))).show().on('error', function() {
|
||||
$(this).hide();
|
||||
});
|
||||
}
|
||||
|
||||
// Имя файла картинки для превью: если список карт загружен, берём реальный
|
||||
// файл (он может быть png/webp), иначе считаем, что это jpg.
|
||||
function mapImageFileFor(mapName) {
|
||||
var found = _.find(mapImages || [], function(m) {
|
||||
return m.name === mapName;
|
||||
});
|
||||
if (found) return found.file;
|
||||
return /\.(jpg|jpeg|png|webp)$/i.test(mapName) ? mapName : mapName + '.jpg';
|
||||
}
|
||||
|
||||
// Догружает список картинок, чтобы превью находили файлы любого формата.
|
||||
function ensureMapImages() {
|
||||
if (mapImages) return;
|
||||
$.ajax({
|
||||
url: '/decider/admin/maps',
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
mapImages = data;
|
||||
$('#mapsContainer .map-row').each(function() {
|
||||
updateMapRow($(this));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove map row
|
||||
function removeMapRow(btn) {
|
||||
$(btn).closest('.map-row').remove();
|
||||
}
|
||||
|
||||
// Collect form data and save
|
||||
function saveDecider() {
|
||||
var key = $('#formKey').val().trim();
|
||||
if (!key) {
|
||||
alert('Key is required');
|
||||
return;
|
||||
}
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
|
||||
alert('Key may contain only letters, digits, hyphen and underscore');
|
||||
return;
|
||||
}
|
||||
|
||||
var maps = [];
|
||||
$('#mapsContainer .map-row').each(function() {
|
||||
var mapName = $(this).find('.map-name').val().trim();
|
||||
var desc = $(this).find('.map-desc').val().trim();
|
||||
if (mapName) {
|
||||
maps.push({ map: mapName, description: desc || null });
|
||||
}
|
||||
});
|
||||
|
||||
var data = {
|
||||
key: key,
|
||||
name: $('#formName').val().trim(),
|
||||
rules: $('#formRules').val(),
|
||||
raceCount: parseInt($('#formRaceCount').val()) || 1,
|
||||
existAdditionalRaces: $('#formExistAdditionalRaces').is(':checked'),
|
||||
existTextFieldRaces: $('#formExistTextFieldRaces').is(':checked'),
|
||||
description: $('#formDescription').is(':checked'),
|
||||
maps: maps
|
||||
};
|
||||
|
||||
var msgEl = $('#saveMessage');
|
||||
msgEl.html('<span class="text-muted">Saving...</span>');
|
||||
|
||||
var url = '/decider/admin/deciders';
|
||||
var method = 'POST';
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
method: method,
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data),
|
||||
success: function() {
|
||||
msgEl.html('<span class="success-msg">Saved!</span>');
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
},
|
||||
error: function(xhr) {
|
||||
var error = '';
|
||||
try { error = JSON.parse(xhr.responseText).error; } catch(e) {}
|
||||
msgEl.html('<span class="error-msg">Error: ' + error + '</span>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delete current decider
|
||||
function deleteCurrentDecider() {
|
||||
if (!currentDeciderKey) return;
|
||||
var deciderName = $('#formName').val();
|
||||
$('#deleteDeciderName').text(deciderName + ' (' + currentDeciderKey + ')');
|
||||
$('#deleteModal').modal('show');
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
var key = currentDeciderKey;
|
||||
$.ajax({
|
||||
url: '/decider/admin/deciders/' + encodeURIComponent(key),
|
||||
method: 'DELETE',
|
||||
success: function() {
|
||||
location.reload();
|
||||
},
|
||||
error: function() {
|
||||
alert('Failed to delete decider');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize: if there's only one decider, open it automatically
|
||||
$(document).ready(function() {
|
||||
// Nothing to do on load - user selects from list
|
||||
$('#mapPickerModal').on('hidden.bs.modal', function() {
|
||||
pickerTargetRow = null;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
<div style="clear: both"></div>
|
||||
<div><i><b>Правила турнира "@deciderHumanName"</b><br/>
|
||||
<p>@rules</p>
|
||||
<p style="white-space: pre-wrap">@rules</p>
|
||||
<br>
|
||||
</i>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user