New Filestructure for Docker
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
RewriteEngine On
|
||||
|
||||
ErrorDocument 429 /error-pages/429.html
|
||||
ErrorDocument 403 /error-pages/403.html
|
||||
ErrorDocument 500 /error-pages/500.html
|
||||
ErrorDocument 400 /error-pages/400.html
|
||||
ErrorDocument 404 /error-pages/400.html
|
||||
|
||||
RewriteRule ^(riegeneinteilung)$ $1.php [L]
|
||||
@@ -0,0 +1,30 @@
|
||||
RewriteEngine On
|
||||
|
||||
ErrorDocument 503 /503.html
|
||||
|
||||
RewriteCond %{REQUEST_URI} !^/503\.html$
|
||||
|
||||
RewriteRule ^(index)$ $1.php [L]
|
||||
RewriteRule ^(404|503)$ $1.html [L]
|
||||
|
||||
# ----------------------------------------
|
||||
# 1) Root → index.php
|
||||
# ----------------------------------------
|
||||
RewriteRule ^$ router.php [L]
|
||||
|
||||
# ----------------------------------------
|
||||
# 2) Allow existing files
|
||||
# ----------------------------------------
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# ----------------------------------------
|
||||
# 3) Allow existing directories
|
||||
# ----------------------------------------
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# ----------------------------------------
|
||||
# 4) Everything else → router.php
|
||||
# ----------------------------------------
|
||||
RewriteRule ^ router.php [QSA,L]
|
||||
@@ -0,0 +1,344 @@
|
||||
const selecteddiscipline = window.FREIGABE;
|
||||
|
||||
const csrf_token = window.CSDR_TOKEN;
|
||||
|
||||
let ws;
|
||||
let firstConnect = true;
|
||||
let wsOpen = false;
|
||||
const RETRY_DELAY = 5000;
|
||||
|
||||
const WSaccesstype = "rankLive";
|
||||
const WSaccess = window.FREIGABE;
|
||||
const WSaccessPermission = "R";
|
||||
|
||||
const urlAjaxNewWSToken = '/intern/scripts/ajax-create-ws-token.php';
|
||||
|
||||
async function fetchNewWSToken(accesstype, access, accessPermission) {
|
||||
try {
|
||||
const response = await fetch(urlAjaxNewWSToken, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
csrf_token,
|
||||
accesstype,
|
||||
access,
|
||||
accessPermission
|
||||
})
|
||||
});
|
||||
|
||||
if (response.status === 403) {
|
||||
console.warn("Please Re-Autenithicate. Reloading page...");
|
||||
location.reload();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const data = await response.json();
|
||||
return data.success ? data.token : null;
|
||||
} catch (error) {
|
||||
console.error("Token fetch failed:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function startWebSocket() {
|
||||
console.log("Attempting WebSocket connection...");
|
||||
|
||||
let token;
|
||||
if (firstConnect) {
|
||||
token = window.WS_ACCESS_TOKEN;
|
||||
} else {
|
||||
token = await fetchNewWSToken(WSaccesstype, WSaccess, WSaccessPermission);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.error("No valid token available. Retrying...");
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ws = new WebSocket(`wss://${window.location.hostname}/ws/?access=token&token=${token}`);
|
||||
} catch (err) {
|
||||
console.error("Malformed WebSocket URL", err);
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket connected!");
|
||||
if (!firstConnect) {
|
||||
displayMsg(1, "Live Syncronisation wieder verfügbar");
|
||||
}
|
||||
firstConnect = true;
|
||||
wsOpen = true;
|
||||
};
|
||||
|
||||
ws.onerror = (event) => {
|
||||
console.error("WebSocket error observed." + JSON.stringify(event));
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
|
||||
if (firstConnect) {
|
||||
displayMsg(0, "Live Syncronisation verloren");
|
||||
}
|
||||
firstConnect = false;
|
||||
wsOpen = false;
|
||||
|
||||
scheduleRetry();
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleRetry() {
|
||||
console.log(`Retrying in ${RETRY_DELAY}ms...`);
|
||||
setTimeout(startWebSocket, RETRY_DELAY);
|
||||
}
|
||||
|
||||
// Start the initial connection attempt safely
|
||||
startWebSocket();
|
||||
|
||||
let rangNotenArray = JSON.parse(window.RANG_NOTEN_ARRAY) ?? [];
|
||||
|
||||
const rangNotenId = window.RANG_NOTE_ID ?? 0;
|
||||
|
||||
const rangSort = window.RANG_SORT ?? '';
|
||||
const isLive = parseInt(window.LIVE ?? 0);
|
||||
|
||||
ws.addEventListener("message", async function(event) {
|
||||
let msgOBJ;
|
||||
|
||||
try {
|
||||
msgOBJ = JSON.parse(event.data);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure it's an UPDATE type (matches your sendToGroup logic)
|
||||
if (msgOBJ?.type === "UPDATE") {
|
||||
const data = msgOBJ.payload;
|
||||
|
||||
// Check access rights
|
||||
if (data.discipline === selecteddiscipline.toLowerCase() || selecteddiscipline === 'A') {
|
||||
const noten = data.rankLive;
|
||||
|
||||
const programmId = $(`tr[data-person-id="${data.personId}"]`).closest('tbody').attr('data-programm-id') ?? null;
|
||||
|
||||
if (!isLive && noten[0][1][rangNotenId] !== undefined && rangNotenArray[programmId][data.personId] !== undefined) {
|
||||
rangNotenArray[programmId][data.personId] = noten[0][1][rangNotenId];
|
||||
applyRanks(rankProgramm(rangNotenArray, programmId, rangSort), true);
|
||||
}
|
||||
|
||||
for (const [keyG, noteGroup] of Object.entries(noten)) {
|
||||
|
||||
for (const [run, runGroup] of Object.entries(noteGroup)) {
|
||||
|
||||
for (const [key, value] of Object.entries(runGroup)) {
|
||||
|
||||
const $elements = $(`.changebleValue[data-field-type-id="${key}"][data-geraet-id="${keyG}"][data-person-id="${data.personId}"][data-run="${run}"]`);
|
||||
$elements.each(function() {
|
||||
const $el = $(this);
|
||||
|
||||
const isNullable = ($el.attr('data-noten-nullable') ?? 'false') === 'true';
|
||||
const floatValZero = parseFloat(value.replace(",", ".").replace("'", "")) === 0.0;
|
||||
|
||||
if (isNullable && floatValZero || value === '') {
|
||||
$el.text('-').addClass('emtyPlaceholder');
|
||||
} else {
|
||||
$el.text(value).removeClass('emtyPlaceholder');
|
||||
}
|
||||
|
||||
$el.removeClass('updated');
|
||||
$el.addClass('updated');
|
||||
|
||||
updateDependentVisibility($el);
|
||||
|
||||
setTimeout(() => {
|
||||
$el.removeClass('updated');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (msgOBJ?.type === "UPDATE_RANKLIVE_C_SUBABT") {
|
||||
if (!(await displayConfirm('Eine neue Gruppe ist aktiv. Fenster aktualisieren?'))) return;
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
function rankProgramm(dataObj, programmId, order = 'DESC') {
|
||||
if (!dataObj[programmId]) {
|
||||
console.error(`ID ${programmId} not found in data.`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const values = dataObj[programmId];
|
||||
|
||||
// 1. Transform and Sort
|
||||
const sorted = Object.entries(values)
|
||||
.map(([id, val]) => ({ id, val }))
|
||||
.sort((a, b) => {
|
||||
// 1. Handle null values first (Always push them to the bottom)
|
||||
if (a.val === null && b.val === null) return 0;
|
||||
if (a.val === null) return 1; // Move 'a' to a higher index (down)
|
||||
if (b.val === null) return -1; // Move 'b' to a higher index (down)
|
||||
|
||||
// 2. Normal sorting logic for valid numbers
|
||||
return order.toUpperCase() === 'ASC'
|
||||
? a.val - b.val
|
||||
: b.val - a.val;
|
||||
});
|
||||
|
||||
let lastVal = null;
|
||||
let lastRank = 0;
|
||||
|
||||
let sortedMap = [];
|
||||
|
||||
sortedMap[programmId] = sorted.map((item, index) => {
|
||||
if (item.val !== lastVal) {
|
||||
lastRank = index + 1;
|
||||
}
|
||||
|
||||
lastVal = item.val;
|
||||
|
||||
return {
|
||||
rang: lastRank,
|
||||
...item
|
||||
};
|
||||
});
|
||||
|
||||
return sortedMap;
|
||||
}
|
||||
|
||||
function rankAll(dataObj, order = 'DESC') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(dataObj).map(([groupKey, values]) => {
|
||||
const sorted = Object.entries(values)
|
||||
.map(([id, val]) => ({ id, val }))
|
||||
.sort((a, b) => {
|
||||
// 1. Handle null values first (Always push them to the bottom)
|
||||
if (a.val === null && b.val === null) return 0;
|
||||
if (a.val === null) return 1; // Move 'a' to a higher index (down)
|
||||
if (b.val === null) return -1; // Move 'b' to a higher index (down)
|
||||
|
||||
// 2. Normal sorting logic for valid numbers
|
||||
return order.toUpperCase() === 'ASC'
|
||||
? a.val - b.val
|
||||
: b.val - a.val;
|
||||
});
|
||||
|
||||
let lastVal = null;
|
||||
let lastRank = 0;
|
||||
|
||||
const ranked = sorted.map((item, index) => {
|
||||
if (item.val !== lastVal) {
|
||||
lastRank = index + 1;
|
||||
}
|
||||
lastVal = item.val;
|
||||
|
||||
return {
|
||||
rang: lastRank,
|
||||
...item
|
||||
};
|
||||
});
|
||||
|
||||
return [groupKey, ranked];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function applyRanks(array, sort = false) {
|
||||
for (const [key, entries] of Object.entries(array)) {
|
||||
const $tbody = $(`tbody[data-programm-id="${key}"]`);
|
||||
|
||||
if ($tbody.length === 0) continue;
|
||||
|
||||
for (const [skey, entry] of Object.entries(entries)) {
|
||||
const $row = $tbody.find(`tr[data-person-id="${entry.id}"]`);
|
||||
|
||||
if ($row.length === 0) continue;
|
||||
|
||||
$row.attr('data-sort-rank', entry.rang);
|
||||
|
||||
const $cell = $row.find('.rangField');
|
||||
|
||||
const points = parseFloat(rangNotenArray[key][entry.id]);
|
||||
|
||||
const text = !isNaN(points) ? entry.rang : '';
|
||||
|
||||
$cell.text(text);
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
const rows = $tbody.find('tr').get();
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const rankA = parseFloat($(a).attr('data-sort-rank')) || 999;
|
||||
const rankB = parseFloat($(b).attr('data-sort-rank')) || 999;
|
||||
return rankA - rankB;
|
||||
});
|
||||
|
||||
// Re-append the sorted rows back into the tbody
|
||||
$.each(rows, (index, row) => {
|
||||
$tbody.append(row);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyRanks(rankAll(rangNotenArray, rangSort), true);
|
||||
|
||||
function updateDependentVisibility($changedEl) {
|
||||
const uid = $changedEl.attr('data-uid');
|
||||
if (uid === undefined || uid === null) return;
|
||||
|
||||
const $row = $changedEl.closest('tr');
|
||||
|
||||
if ($row.length !== 1) return;
|
||||
|
||||
const isVisible = $changedEl.hasClass('emtyPlaceholder');
|
||||
const doesExist = $changedEl.hasClass('nonExistentEl');
|
||||
|
||||
const $linkedEl = $row.find(`.singleValueSpan[data-linked-el-uid="${uid}"]`)
|
||||
|
||||
$linkedEl.toggleClass('emtyPlaceholder', isVisible).toggleClass('nonExistentEl', doesExist);
|
||||
}
|
||||
|
||||
function initConditionalVisibility() {
|
||||
const $table = $('table.customDisplayEditorTable');
|
||||
|
||||
const $rows = $table.find('tr');
|
||||
|
||||
$rows.each((ind, el) => {
|
||||
const $row = $(el);
|
||||
$row.find('.singleValueSpan[data-linked-el-uid]').each(function() {
|
||||
const linkedUid = $(this).data('linked-el-uid');
|
||||
|
||||
const $linkedEl = $row.find(`.singleValueSpan[data-uid="${linkedUid}"]`);
|
||||
|
||||
if ($linkedEl.length === 1) {
|
||||
updateDependentVisibility($linkedEl);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
initConditionalVisibility();
|
||||
initConditionalVisibility();
|
||||
|
||||
const $parent = $('.allAbtContainer');
|
||||
const $children = $parent.children('.shiftedGeraetTable');
|
||||
|
||||
$children.sort((a, b) => {
|
||||
const idA = parseFloat($(a).data('geraet-index'));
|
||||
const idB = parseFloat($(b).data('geraet-index'));
|
||||
|
||||
return idA - idB;
|
||||
});
|
||||
|
||||
$parent.append($children);
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
$request = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
|
||||
$segments = $request === '' ? [] : explode('/', $request);
|
||||
|
||||
$segments = array_map('urldecode', $segments);
|
||||
|
||||
$type = $segments[1] ?? null;
|
||||
$slug = $segments[2] ?? null;
|
||||
|
||||
$allowed_types = ["programm", "live"];
|
||||
|
||||
if (($type !== null && !in_array($type, $allowed_types)) || count($segments) > 3) {
|
||||
http_response_code(400);
|
||||
include $baseDir . "/error-pages/400.html";
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
define('APP_ROUTER', true);
|
||||
|
||||
require __DIR__ . "/script.php";
|
||||
@@ -0,0 +1,667 @@
|
||||
<?php
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
if (!isset($segments) || !defined('APP_ROUTER')) {
|
||||
http_response_code(403);
|
||||
include $baseDir . "/error-pages/403.php";
|
||||
exit;
|
||||
}
|
||||
|
||||
$type = $segments[1] ?? null;
|
||||
$slug = $segments[2] ?? null;
|
||||
|
||||
$allowed_types = ["programm", "live"];
|
||||
|
||||
if ($type !== null && !in_array($type, $allowed_types)) {
|
||||
http_response_code(400);
|
||||
include $baseDir . "/error-pages/400.html";
|
||||
exit;
|
||||
}
|
||||
|
||||
$baseDir = $_SERVER['DOCUMENT_ROOT'];
|
||||
|
||||
require $baseDir . '/../scripts/db/db-verbindung-script-guest.php';
|
||||
|
||||
require $baseDir . "/../scripts/db/db-tables.php";
|
||||
require $baseDir . "/../scripts/db/db-functions.php";
|
||||
|
||||
$rankLive_public = db_get_variable($guest, $db_tabelle_var, ['rankLivePublic']) ?? 0;
|
||||
|
||||
require $baseDir . '/../scripts/websocket/ws-create-token.php';
|
||||
|
||||
require_once $baseDir . '/../scripts/session_functions.php';
|
||||
|
||||
ini_wkvs_session(true);
|
||||
|
||||
$csrf_token = $_SESSION['csrf_token'] ?? '';
|
||||
|
||||
if (!$rankLive_public) {
|
||||
check_user_permission('wk_leitung', false, true);
|
||||
}
|
||||
|
||||
$is_live = $type === 'live';
|
||||
|
||||
if ($slug !== null && !$is_live) {
|
||||
|
||||
$alle_programme_db = db_select($guest, $db_tabelle_kategorien, '`programm`', 'aktiv = ?', ['1']);
|
||||
$raw_programme = array_column($alle_programme_db, 'programm', 'programm');
|
||||
|
||||
$alle_programme = [];
|
||||
foreach ($raw_programme as $prog) {
|
||||
$normalized_key = str_replace(' ', '-', strtolower($prog));
|
||||
$alle_programme[$normalized_key] = $prog;
|
||||
}
|
||||
|
||||
$req_prog = str_replace(' ', '-', strtolower($slug));
|
||||
|
||||
if (!isset($alle_programme[$req_prog])) {
|
||||
http_response_code(400);
|
||||
$message = 'Das gesuchte Programm kann nicht gefunden werden';
|
||||
include $baseDir . "/error-pages/400.php";
|
||||
exit;
|
||||
}
|
||||
|
||||
$validated_programm_name = $alle_programme[$req_prog];
|
||||
}
|
||||
|
||||
$selectedFreigabeId = 'A';
|
||||
|
||||
$json_type = $is_live ? 'rankLive-geraet' : 'rankLive-overview';
|
||||
|
||||
$jsonstr = db_select($guest, $db_tabelle_tabellen_konfiguration, 'json', 'type_slug = ?', [$json_type], '', 1);
|
||||
|
||||
$data = json_decode($jsonstr[0]['json'], true);
|
||||
|
||||
$headers = $data['header'] ?? [];
|
||||
$bodyColumns = $data['body'] ?? [];
|
||||
|
||||
$alle_programme_db = db_select($guest, $db_tabelle_kategorien, '`programm`, `id`', 'aktiv = ?', ['1']);
|
||||
$indexedProgrammes = array_column($alle_programme_db, 'programm', 'id');
|
||||
|
||||
$personData = [
|
||||
'geburtsdatum' => 'Geburtsdatum',
|
||||
'name' => 'Name',
|
||||
'vorname' => 'Vorname',
|
||||
'verein' => 'Verein',
|
||||
'programm' => 'Programm'
|
||||
];
|
||||
|
||||
$disciplines = db_select($guest, $db_tabelle_disziplinen, '*', '', [], 'start_index ASC');
|
||||
|
||||
$indexedArrayGeraete = array_column($disciplines, 'name', 'id');
|
||||
|
||||
if ($is_live) {
|
||||
$indexed_array_geraete_id_index = array_column($disciplines, 'start_index', 'id');
|
||||
|
||||
$indexed_array_geraete_index_id = array_flip($indexed_array_geraete_id_index);
|
||||
|
||||
$abt = (int) (db_get_variable($guest, $db_tabelle_var, ['wk_panel_current_abt']) ?? 1);
|
||||
$akt_subabt = (int) (db_get_variable($guest, $db_tabelle_var, ['wk_panel_current_subabt_admin']) ?? 1);
|
||||
|
||||
$max_subabt = count($disciplines);
|
||||
|
||||
$stmt = $guest->prepare("SELECT
|
||||
t.id,
|
||||
t.name,
|
||||
t.vorname,
|
||||
t.programm,
|
||||
t.verein,
|
||||
t.geburtsdatum,
|
||||
p.id AS programm_id,
|
||||
tabt.`turnerin_index` AS start_index,
|
||||
tabt.`geraet_id` AS startgeraet,
|
||||
tabt.`abteilung_id` AS abt_id
|
||||
FROM $db_tabelle_teilnehmende t
|
||||
LEFT JOIN $db_tabelle_kategorien p ON p.programm = t.programm
|
||||
INNER JOIN $db_tabelle_teilnehmende_gruppen tabt ON tabt.`turnerin_id` = t.`id`
|
||||
INNER JOIN $db_tabelle_gruppen abt ON abt.`id` = tabt.`abteilung_id`
|
||||
WHERE (t.bezahlt = 4 OR t.bezahltoverride = 4) AND abt.`order_index` = ?");
|
||||
|
||||
$stmt->bind_param("i", $abt);
|
||||
} elseif ($type === 'programm' && $slug !== null) {
|
||||
$stmt = $guest->prepare("SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.vorname,
|
||||
u.programm,
|
||||
u.verein,
|
||||
u.geburtsdatum,
|
||||
p.id AS programm_id
|
||||
FROM $db_tabelle_teilnehmende u
|
||||
LEFT JOIN $db_tabelle_kategorien p ON p.programm = u.programm
|
||||
WHERE (u.bezahlt = 4 OR u.bezahltoverride = 4) AND u.programm = ?");
|
||||
|
||||
$stmt->bind_param("s", $validated_programm_name);
|
||||
} else {
|
||||
$stmt = $guest->prepare("SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.vorname,
|
||||
u.programm,
|
||||
u.verein,
|
||||
u.geburtsdatum,
|
||||
p.id AS programm_id
|
||||
FROM $db_tabelle_teilnehmende u
|
||||
LEFT JOIN $db_tabelle_kategorien p ON p.programm = u.programm
|
||||
WHERE (u.bezahlt = 4 OR u.bezahltoverride = 4)");
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->get_result();
|
||||
$tures = $result->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
$stmt->close();
|
||||
|
||||
$indexedTures = [];
|
||||
|
||||
foreach ($tures as $s_tures) {
|
||||
$indexedTures[$s_tures['programm_id']][$s_tures['id']] = $s_tures;
|
||||
}
|
||||
|
||||
$current_wk_id = (int) db_get_variable($guest, $db_tabelle_var, ['current_wk_id']) ?: 0;
|
||||
|
||||
$stmt = $guest->prepare("SELECT `note_bezeichnung_id`, `geraet_id`, `person_id`, `run_number`, `public_value`
|
||||
FROM $db_tabelle_wertungen
|
||||
WHERE `is_public` = 1
|
||||
AND `wk_id` = ?");
|
||||
|
||||
$stmt->bind_param("i", $current_wk_id);
|
||||
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->get_result();
|
||||
$noten = $result->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
$stmt->close();
|
||||
|
||||
$indexedNoten = [];
|
||||
|
||||
$stmt = $guest->prepare("SELECT `id`, `geraete_json`, `pro_geraet`, `anzahl_laeufe_json`, `name`, `default_value` FROM $db_tabelle_wertungstypen");
|
||||
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->get_result();
|
||||
$notenConfig = $result->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
$stmt->close();
|
||||
|
||||
$disciplines = db_select($guest, $db_tabelle_disziplinen, '*', '', [], 'start_index ASC');
|
||||
|
||||
$indexedNotenConfig = array_column($notenConfig, null, 'id');
|
||||
|
||||
$rangNote = intval(db_get_var($guest, "SELECT `value` FROM $db_tabelle_var WHERE `name` = ?", ['rangNote']));
|
||||
|
||||
$orderBestRang = db_get_var($guest, "SELECT `value` FROM $db_tabelle_var WHERE `name` = ?", ['orderBestRang']);
|
||||
|
||||
$okValuesOrderBestRang = ["ASC", "DESC"];
|
||||
|
||||
$rangOrderOk = in_array($orderBestRang, $okValuesOrderBestRang) && intval($rangNote) > 0;
|
||||
|
||||
$notenIndexed = [];
|
||||
|
||||
foreach ($noten as $sn) {
|
||||
$notenIndexed[$sn['person_id']][$sn['geraet_id']][$sn['note_bezeichnung_id']][$sn['run_number']] = $sn['public_value'];
|
||||
}
|
||||
|
||||
$disciplinesExtended = array_merge(
|
||||
[["id" => 0, "name" => "None"]],
|
||||
$disciplines
|
||||
);
|
||||
|
||||
$arrayIndexedNoten = [];
|
||||
|
||||
|
||||
$wkName = db_get_var($guest, "SELECT `value` FROM $db_tabelle_var WHERE `name` = ?", ['wkName']);
|
||||
|
||||
$grouped = [];
|
||||
|
||||
function groop_by_key($array, $array_key, bool $int = false): array
|
||||
{
|
||||
$grouped = [];
|
||||
if ($int) {
|
||||
foreach ($array as $entry) {
|
||||
$key = (int) $entry[$array_key];
|
||||
$grouped[$key][] = $entry;
|
||||
}
|
||||
} else {
|
||||
foreach ($array as $entry) {
|
||||
$key = $entry[$array_key];
|
||||
$grouped[$key][] = $entry;
|
||||
}
|
||||
}
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
if ($is_live) {
|
||||
|
||||
$grouped = groop_by_key($tures, 'startgeraet', true);
|
||||
|
||||
} else {
|
||||
|
||||
$grouped = groop_by_key($tures, 'programm');
|
||||
|
||||
foreach ($grouped as $key => $group) {
|
||||
$orderIndexes[$key] = $indexedProgramme[$key]['order_index'] ?? 10000;
|
||||
}
|
||||
|
||||
if (isset($orderIndexes)) {
|
||||
uksort($grouped, function ($a, $b) use ($orderIndexes) {
|
||||
return $orderIndexes[$a] <=> $orderIndexes[$b];
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
foreach ($grouped as $entry_key => $entries_group):
|
||||
|
||||
|
||||
endforeach;
|
||||
|
||||
$timeZone = new \DateTimeZone('Europe/Zurich');
|
||||
|
||||
$formatter = new \IntlDateFormatter(
|
||||
'de_CH',
|
||||
\IntlDateFormatter::FULL,
|
||||
\IntlDateFormatter::NONE,
|
||||
$timeZone
|
||||
);
|
||||
|
||||
$display_over_array = [];
|
||||
|
||||
function constructSingleValueSpanPublic($token, $type, $row, $selected_geraet_id = 0)
|
||||
{
|
||||
global $formatter;
|
||||
global $personData;
|
||||
global $arrayIndexedNoten;
|
||||
global $timeZone;
|
||||
global $display_over_array;
|
||||
global $indexedNotenConfig;
|
||||
global $indexedProgramme;
|
||||
|
||||
$text = '';
|
||||
$classes = 'singleValueSpan';
|
||||
|
||||
$bold = $token['bold'] ?? false;
|
||||
|
||||
$font_size = intval($token['fontSize'] ?? 0);
|
||||
|
||||
$min_display_width = intval($token['minDisplayWidth'] ?? 1) - 1;
|
||||
|
||||
$linked_el_uid = (int) ($token['linkedElUid'] ?? 0);
|
||||
|
||||
$uid = (int) ($token['uid'] ?? 0);
|
||||
|
||||
|
||||
$style = '';
|
||||
|
||||
if ($font_size > 0) {
|
||||
$style = ' style="font-size: ' . $font_size . 'px"';
|
||||
}
|
||||
|
||||
if ($min_display_width > 0) {
|
||||
$classes .= ' displayOver' . $min_display_width . 'px';
|
||||
$display_over_array[] = $min_display_width;
|
||||
}
|
||||
|
||||
$dataAttributes = 'data-bold="' . htmlspecialchars($bold) . '" data-uid="'. $uid .'"';
|
||||
|
||||
if ($linked_el_uid > 0) {
|
||||
$dataAttributes .= ' data-linked-el-uid="' . $linked_el_uid . '"';
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'text':
|
||||
$text = $token['text'] ?? '';
|
||||
$classes .= ' staticText';
|
||||
break;
|
||||
|
||||
case 'sortData':
|
||||
$column = $token['column'] ?? '';
|
||||
switch ($column) {
|
||||
case "rang":
|
||||
$classes .= ' rangField';
|
||||
break;
|
||||
case "startindex":
|
||||
$text = $row['calculedstartindex'];
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'personData':
|
||||
$column = $token['column'] ?? '';
|
||||
|
||||
if ($column === 'jahrgang') {
|
||||
$geburtsdatum = $row['geburtsdatum'] ?? '';
|
||||
$date = new \DateTimeImmutable($geburtsdatum, $timeZone);
|
||||
$formatter->setPattern('yyyy');
|
||||
|
||||
$text = $formatter->format($date);
|
||||
} else {
|
||||
$text = array_key_exists($column, $personData) ? $row[$column] ?? '' : '';
|
||||
|
||||
if ($column === 'geburtsdatum') {
|
||||
$date = new \DateTimeImmutable($text, $timeZone);
|
||||
$formatter->setPattern(($token['phpDateFormat'] ?? '' !== '') ? $token['phpDateFormat'] : 'd. MMMM YYYY');
|
||||
|
||||
$text = $formatter->format($date);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'notenGeraetData':
|
||||
|
||||
$field_type_id = intval($token['field-type-id'] ?? 0);
|
||||
$run = intval($token['run'] ?? 0);
|
||||
$person_id = $row['id'];
|
||||
|
||||
$programm_id = $indexedProgramme[$row['programm']]['id'] ?? 0;
|
||||
|
||||
$runsJSON = json_decode($indexedNotenConfig[$field_type_id]['anzahl_laeufe_json'] ?? '', true) ?? [];
|
||||
|
||||
$max_run = $runsJSON[$selected_geraet_id][$programm_id] ?? $runsJSON[$selected_geraet_id]['all'] ?? $runsJSON['default'] ?? 0;
|
||||
|
||||
if ($run > $max_run) {
|
||||
$classes .= ' nonExistentEl';
|
||||
|
||||
} else {
|
||||
$notenNullable = ($token['notenNullable'] ?? 'false') === 'true';
|
||||
|
||||
$dataAttributes .= ' data-field-type-id="' . $field_type_id . '"';
|
||||
$dataAttributes .= ' data-geraet-id="' . $selected_geraet_id . '"';
|
||||
$dataAttributes .= ' data-person-id="' . $person_id . '"';
|
||||
$dataAttributes .= ' data-run="' . $run . '"';
|
||||
$dataAttributes .= ' data-noten-nullable="' . (($notenNullable) ? "true" : "false") . '"';
|
||||
|
||||
$text = $arrayIndexedNoten[$person_id][$selected_geraet_id][$field_type_id][$run]['value'] ?? null;
|
||||
|
||||
if ($notenNullable && $text !== null && floatval($text) === 0.0) {
|
||||
$text = ' ';
|
||||
}
|
||||
|
||||
if ($text === null) {
|
||||
$classes .= ' emtyPlaceholder';
|
||||
$text = '-';
|
||||
}
|
||||
|
||||
$classes .= ' changebleValue';
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
|
||||
|
||||
case 'notenData':
|
||||
$field_type_id = intval($token['field-type-id'] ?? 0);
|
||||
$geraet_id = intval($token['geraet-id'] ?? 0);
|
||||
$run = intval($token['run'] ?? 0);
|
||||
$person_id = $row['id'];
|
||||
$notenNullable = ($token['notenNullable'] ?? 'false') === 'true';
|
||||
|
||||
$dataAttributes .= ' data-field-type-id="' . $field_type_id . '"';
|
||||
$dataAttributes .= ' data-geraet-id="' . $geraet_id . '"';
|
||||
$dataAttributes .= ' data-person-id="' . $person_id . '"';
|
||||
$dataAttributes .= ' data-run="' . $run . '"';
|
||||
$dataAttributes .= ' data-noten-nullable="' . (($notenNullable) ? "true" : "false") . '"';
|
||||
|
||||
$text = $arrayIndexedNoten[$person_id][$geraet_id][$field_type_id][$run]['value'] ?? null;
|
||||
|
||||
if ($notenNullable && $text !== null && floatval($text) === 0.0) {
|
||||
$text = ' ';
|
||||
}
|
||||
|
||||
if ($text === null) {
|
||||
$classes .= ' emtyPlaceholder';
|
||||
$text = '-';
|
||||
}
|
||||
|
||||
$classes .= ' changebleValue';
|
||||
|
||||
break;
|
||||
}
|
||||
?>
|
||||
<span class="<?= $classes ?>" <?= $dataAttributes; ?><?= $style ?>><?= htmlspecialchars($text); ?></span>
|
||||
<?php }
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de-ch">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="png" href="/intern/img/icon.png">
|
||||
<link rel="stylesheet" href="/intern/css/custom-msg-display.css">
|
||||
<link href="/files/fonts/fonts.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/custom-display-editor-required-css.css">
|
||||
<link rel="stylesheet" href="/css/rank-live.css">
|
||||
<link rel="stylesheet" href="/css/header.css">
|
||||
<script src="/intern/js/jquery/jquery-3.7.1.min.js"></script>
|
||||
<script src="/intern/js/custom-msg-display.js"></script>
|
||||
<script src="/js/header.js"></script>
|
||||
<title>RankLive - <?= htmlspecialchars($wkName) ?></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section class="bgSection">
|
||||
<?php include $baseDir . '/../scripts/public-elements/rankLive-header.php'; ?>
|
||||
<?php if (count($indexedTures) < 1) : ?>
|
||||
<h3>Noch keine Daten verfügbar</h3>
|
||||
<?php exit; ?>
|
||||
<?php endif; ?>
|
||||
<div class="headerWrapper">
|
||||
<h2 class="headerMain">RankLive</h2>
|
||||
<?php if ($is_live) : ?>
|
||||
<h4>Gruppe: <?= (int) $abt ?></h4>
|
||||
<h4>Rotation: <?= (int) $akt_subabt ?></h4>
|
||||
<?php elseif ($type === 'programm' && $slug !== null) : ?>
|
||||
<h4><?= htmlspecialchars($validated_programm_name) ?></h4>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="allAbtContainer">
|
||||
<?php foreach ($grouped as $entry_key => $entries_group):
|
||||
|
||||
$table_data_geraet_label = '';
|
||||
$extra_table_classes = '';
|
||||
|
||||
if ($is_live) {
|
||||
$entries_with_calculated_index = [];
|
||||
|
||||
// Optional: Vorab-Caching der MAX-Indices, um DB-Abfragen im Loop zu vermeiden
|
||||
// (Nur aktivieren, wenn $entries_group sehr groß ist und Performance-Probleme auftreten)
|
||||
$maxStartIndexCache = [];
|
||||
|
||||
foreach ($entries_group as $row) {
|
||||
$old_geraet_id = $row['startgeraet'];
|
||||
|
||||
$old_geraet_index = $indexed_array_geraete_id_index[$row['startgeraet']];
|
||||
$shifted_geraet_index = $old_geraet_index + $akt_subabt - 1;
|
||||
if ($shifted_geraet_index > $max_subabt) {
|
||||
$shifted_geraet_index -= $max_subabt;
|
||||
}
|
||||
$row['startgeraet'] = $indexed_array_geraete_index_id[$shifted_geraet_index];
|
||||
|
||||
$rohstartindex = intval($row['start_index']);
|
||||
$abtId = intval($row['abt_id']);
|
||||
|
||||
// Hole maxStartIndex (mit Caching-Logik optional)
|
||||
if (!isset($maxStartIndexCache["$abtId-$old_geraet_id "])) {
|
||||
$maxStartIndexCache["$abtId-$old_geraet_id"] = db_get_var($guest, "SELECT COUNT(*) FROM `$db_tabelle_teilnehmende_gruppen` WHERE abteilung_id = ? AND geraet_id = ?", [$abtId, $old_geraet_id]);
|
||||
}
|
||||
|
||||
$maxstartindex = $maxStartIndexCache["$abtId-$old_geraet_id"];
|
||||
|
||||
// Sicherheit: Vermeiden Sie Division durch Null oder Modulo durch 0
|
||||
if ($maxstartindex < 1) {
|
||||
$maxstartindex = 1;
|
||||
}
|
||||
|
||||
// The order shift depends on the number of rotations the group has made
|
||||
$rotation_shift = $akt_subabt - 1;
|
||||
|
||||
$calculedstartindex = $rohstartindex - $rotation_shift;
|
||||
|
||||
// Sicherstellen, dass das Ergebnis positiv ist (PHP Modulo kann negative Ergebnisse liefern)
|
||||
// Wenn das Ergebnis negativ ist, addieren wir maxstartindex
|
||||
if ($calculedstartindex < 1) {
|
||||
$calculedstartindex += $maxstartindex;
|
||||
}
|
||||
|
||||
// Kopie der Zeile erstellen, um Originaldaten nicht zu verändern
|
||||
$row['calculedstartindex'] = $calculedstartindex;
|
||||
$entries_with_calculated_index[] = $row;
|
||||
}
|
||||
|
||||
// Sortieren nach dem berechneten Startindex
|
||||
usort($entries_with_calculated_index, fn($a, $b) => $a['calculedstartindex'] <=> $b['calculedstartindex']);
|
||||
|
||||
$entries_to_display = $entries_with_calculated_index;
|
||||
|
||||
$shifted_geraet_index = $indexed_array_geraete_id_index[$entry_key] + $akt_subabt - 1;
|
||||
if ($shifted_geraet_index > $max_subabt) {
|
||||
$shifted_geraet_index -= $max_subabt;
|
||||
}
|
||||
$shifted_geraet_id = $indexed_array_geraete_index_id[$shifted_geraet_index];
|
||||
|
||||
$table_data_geraet_label = ' data-geraet-index="' . $shifted_geraet_index . '"';
|
||||
$extra_table_classes = ' shiftedGeraetTable';
|
||||
} else {
|
||||
$entries_to_display = $entries_group;
|
||||
}
|
||||
|
||||
foreach ($entries_to_display as $row):
|
||||
$rangNotenArray[$entry_key][$row['id']] = isset($notenIndexed[$row['id']][0][$rangNote][1]) ? floatval($notenIndexed[$row['id']][0][$rangNote][1]) : null;
|
||||
foreach ($disciplinesExtended as $discipline):
|
||||
foreach ($notenConfig as $snC):
|
||||
|
||||
if (intval($snC['pro_geraet']) === 1 && intval($discipline['id']) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (intval($snC['pro_geraet']) !== 1) {
|
||||
$allowedGeraete = !empty($snC['geraete_json']) ? json_decode($snC['geraete_json'], true) : [];
|
||||
if (!in_array($discipline['id'], $allowedGeraete)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$runsJSON = !empty($snC['anzahl_laeufe_json']) ? json_decode($snC['anzahl_laeufe_json'], true) : [];
|
||||
|
||||
$runs = $runsJSON[$discipline['id']][$entry_key] ?? $runsJSON[$discipline['id']]['all'] ?? $runsJSON["default"] ?? 1;
|
||||
|
||||
for ($r = 1; $r <= $runs; $r++):
|
||||
$note = $notenIndexed[$row['id']][$discipline['id']][$snC['id']][$r] ?? null;
|
||||
$normalizedNote = ($note !== null) ? number_format($note, $snC['nullstellen'] ?? 2) : null;
|
||||
|
||||
$arrayIndexedNoten[intval($row['id'])][intval($discipline['id'])][intval($snC['id'])][intval($r)] = ["value" => $normalizedNote];
|
||||
endfor;
|
||||
endforeach;
|
||||
endforeach;
|
||||
endforeach; ?>
|
||||
<div class="singleAbtDiv<?= $extra_table_classes ?>" <?= $table_data_geraet_label ?>>
|
||||
<?php if (!($type === 'programm' && $slug !== null)) : ?>
|
||||
<h2 class="headerAbt"><?= ($is_live) ? $indexedArrayGeraete[$shifted_geraet_id] ?? '' : $entry_key ?? '' ?></h2>
|
||||
<?php endif; ?>
|
||||
<?php $geraet_id_for_live_view = $is_live ? $shifted_geraet_id : 0 ?>
|
||||
<div class="tableWraper">
|
||||
<table class="customDisplayEditorTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<?php foreach ($headers as $header_data): ?>
|
||||
<?php
|
||||
$min_display_width = ($header_data['minDisplayWidth'] ?? 1) - 1;
|
||||
|
||||
$class = "";
|
||||
|
||||
if ($min_display_width > 0) {
|
||||
$class = 'displayOver' . $min_display_width . 'px';
|
||||
$display_over_array[] = $min_display_width;
|
||||
}
|
||||
?>
|
||||
|
||||
<th class="<?= $class ?>">
|
||||
<?= htmlspecialchars($header_data['title']) ?>
|
||||
</th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody <?= !$is_live ? 'data-programm-id="'. $entry_key .'"' : ''?>>
|
||||
<?php foreach ($entries_to_display as $row): ?>
|
||||
<tr data-person-id="<?= $row['id'] ?>">
|
||||
<?php foreach ($bodyColumns as $ind => $columnTokens): ?>
|
||||
<?php
|
||||
$class = '';
|
||||
|
||||
if (isset($headers[$ind]['minDisplayWidth']) && $headers[$ind]['minDisplayWidth'] > 1) {
|
||||
$min_display_width = ($headers[$ind]['minDisplayWidth'] ?? 1) - 1;
|
||||
|
||||
$class = 'displayOver' . $min_display_width . 'px';
|
||||
$display_over_array[] = $min_display_width;
|
||||
}
|
||||
?>
|
||||
<td class=<?= $class ?>>
|
||||
<span class="tdSpan">
|
||||
|
||||
<?php
|
||||
foreach ($columnTokens as $token):
|
||||
$type = $token['type'] ?? '';
|
||||
if ($type === 'layoutEls') :
|
||||
$column = $token['column'];
|
||||
$content = $token['content'] ?? []; ?>
|
||||
<div class="<?= $column ?>LayoutEl">
|
||||
<?php foreach ($content as $layout_row) : ?>
|
||||
<span class="<?= $column ?>LayoutElRow">
|
||||
<?php foreach ($layout_row as $el) :
|
||||
constructSingleValueSpanPublic($el, $el['type'], $row, $geraet_id_for_live_view);
|
||||
endforeach; ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else:
|
||||
constructSingleValueSpanPublic($token, $type, $row, $geraet_id_for_live_view);
|
||||
endif;
|
||||
endforeach; ?>
|
||||
</span>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="tableCutter"></div>
|
||||
</section>
|
||||
<style>
|
||||
<?php
|
||||
$unique_display_over_array = array_unique($display_over_array);
|
||||
|
||||
foreach ($unique_display_over_array as $s_size):
|
||||
$s_size = (int)$s_size;
|
||||
?>
|
||||
@media (max-width: <?= $s_size ?>px) {
|
||||
.displayOver<?= $s_size ?>px {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
<?php endforeach; ?>
|
||||
</style>
|
||||
<script>
|
||||
window.FREIGABE = "<?= $selectedFreigabeId; ?>";
|
||||
window.CSDR_TOKEN = "<?= $csrf_token; ?>";
|
||||
window.WS_ACCESS_TOKEN = "<?= generateWSReadToken('rankLive', $selectedFreigabeId) ?>";
|
||||
window.AKTUELLES_JAHR = "<?= $current_wk_id ?>";
|
||||
window.RANG_NOTEN_ARRAY = '<?= json_encode($rangNotenArray, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?>';
|
||||
window.RANG_SORT = "<?= $orderBestRang ?>";
|
||||
window.RANG_NOTE_ID = <?= $rangNote ?>;
|
||||
window.LIVE = <?= (int) $is_live ?>;
|
||||
</script>
|
||||
|
||||
<script src="/RankLive/js/script.js"></script>
|
||||
|
||||
</body>
|
||||
@@ -0,0 +1,152 @@
|
||||
:root {
|
||||
--sb-bg: #f8fafc;
|
||||
--sb-card-bg: #ffffff;
|
||||
--sb-text-main: #36454F;
|
||||
--sb-text-muted: #64748b;
|
||||
--sb-border: #e2e8f0;
|
||||
--sb-hover: #f3f3f3;
|
||||
--sb-radius: 12px;
|
||||
|
||||
/* Newly extracted color variables */
|
||||
--sb-th-text: #1e293b;
|
||||
--sb-th-border: #1e293b;
|
||||
--sb-th-bg: #ffffff;
|
||||
--sb-tr-even-bg: #f8f8f860;
|
||||
--sb-changeable-bg: rgba(37, 99, 235, 0.02);
|
||||
--sb-flash-highlight: #00ff99; /* Green flash for updates */
|
||||
|
||||
--font-numeric: 'Courier New', Courier, monospace;
|
||||
--padding-td: clamp(3px, 2.5vh, 12px) clamp(4px, 2vw, 20px);
|
||||
--padding-th: clamp(2px, 1.5vh, 6px) clamp(4px, 2vw, 20px);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
:root {
|
||||
--sb-bg: #121212; /* Dark main background */
|
||||
--sb-card-bg: #1a1a1a; /* Dark card surfaces */
|
||||
--sb-text-main: #f5f5f5; /* Light grey/white text */
|
||||
--sb-text-muted: #a0a0a0; /* Muted secondary text */
|
||||
--sb-border: #2d2d2d; /* Soft dark border line */
|
||||
--sb-hover: #252525; /* Subtle highlight on row hover */
|
||||
--sb-radius: 12px;
|
||||
|
||||
/* Dark mode specific variables */
|
||||
--sb-th-text: #ffffff; /* Crisp white headers */
|
||||
--sb-th-border: #3d3d3d; /* Distinct header separation */
|
||||
--sb-th-bg: #121212; /* Matches dark body background */
|
||||
--sb-tr-even-bg: #1a1a1a40; /* Very soft zebra striping */
|
||||
--sb-changeable-bg: rgba(240, 163, 236, 0.05); /* Soft background using the pink/purple accent */
|
||||
--sb-flash-highlight: #39ff14; /* Vibrant neon green for live updates on dark backgrounds */
|
||||
|
||||
--font-numeric: 'Courier New', Courier, monospace;
|
||||
--padding-td: clamp(3px, 2.5vh, 12px) clamp(4px, 2vw, 20px);
|
||||
--padding-th: clamp(2px, 1.5vh, 6px) clamp(4px, 2vw, 20px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Base Table Styling */
|
||||
.customDisplayEditorTable {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
font-size: 0.95rem;
|
||||
color: var(--sb-text-main);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.customDisplayEditorTable th {
|
||||
color: var(--sb-th-text);
|
||||
border-bottom: solid 1px var(--sb-th-border);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.05em;
|
||||
padding: var(--padding-th);
|
||||
white-space: nowrap;
|
||||
position: sticky;
|
||||
background-color: var(--sb-th-bg);
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.customDisplayEditorTable th sup {
|
||||
font-size: 0.65rem;
|
||||
margin-left: 2px;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
.customDisplayEditorTable td {
|
||||
padding: var(--padding-td);
|
||||
border-bottom: 1px solid var(--sb-border);
|
||||
vertical-align: middle;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.customDisplayEditorTable tbody > tr:nth-child(even) td {
|
||||
background-color: var(--sb-tr-even-bg);
|
||||
}
|
||||
|
||||
.customDisplayEditorTable tbody > tr:hover td {
|
||||
background-color: var(--sb-hover) !important;
|
||||
}
|
||||
|
||||
.customDisplayEditorTable td {
|
||||
color: var(--sb-text-main);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.customDisplayEditorTable td.changebleValue {
|
||||
font-family: var(--font-numeric);
|
||||
font-size: 1.05rem;
|
||||
color: var(--sb-text-main);
|
||||
background-color: var(--sb-changeable-bg);
|
||||
}
|
||||
|
||||
.customDisplayEditorTable tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.changebleValue.updated {
|
||||
animation: flashHighlight 3.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes flashHighlight {
|
||||
0% { color: var(--sb-flash-highlight); }
|
||||
100% { color: var(--sb-text-main); }
|
||||
}
|
||||
|
||||
.singleValueSpan[data-bold="true"] {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.staticText {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.tdSpan {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.verticalLayoutEl {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.verticalLayoutElRow {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.verticalLayoutEl .changebleValue {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.emtyPlaceholder {
|
||||
color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nonExistentEl {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
:root {
|
||||
--header-bg: #f8fcffcc;
|
||||
--header-border: 1px solid #ffffff4d;
|
||||
--header-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
|
||||
--header-top: 40px;
|
||||
--header-clamp: clamp(0.5px, 0.225vw, 1px);
|
||||
--accent: #391c36;
|
||||
--menu-box-shadows: rgba(0, 0, 0, 0.1);
|
||||
--menu-dropdown-bg: #fff;
|
||||
--dropdown-el-hover: #f0f0f0;
|
||||
--sidebar-dropdown: rgba(255, 255, 255, 0.4);
|
||||
--sidebar-dropdown-shadow: rgba(0, 0, 0, 0.02);
|
||||
--sidebar-dropdown-el-hover: rgba(0, 0, 0, 0.05);
|
||||
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--header-bg: #000000cc; /* Dark, semi-transparent background with blur support */
|
||||
--header-border: 1px solid #ffffff10; /* Subtle, soft border for dark interfaces */
|
||||
--header-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); /* Stronger shadow to stand out against dark backgrounds */
|
||||
--header-top: 40px;
|
||||
--header-clamp: clamp(0.5px, 0.225vw, 1px);
|
||||
--accent: #f9d8f7; /* Brightened, vibrant purple/pink accent to pop on dark backgrounds */
|
||||
--menu-box-shadows: rgba(0, 0, 0, 0.5); /* Deeper shadow for menu items */
|
||||
--menu-dropdown-bg: #1a1a1a; /* Dark solid background for dropdowns */
|
||||
--dropdown-el-hover: #2a2a2a; /* Slightly lighter dark tone for hover states */
|
||||
--sidebar-dropdown: rgba(30, 30, 30, 0.6); /* Semi-transparent dark background for sidebar dropdowns */
|
||||
--sidebar-dropdown-shadow: rgba(0, 0, 0, 0.2);
|
||||
--sidebar-dropdown-el-hover: rgba(255, 255, 255, 0.08); /* Soft white highlight on hover */
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
top: calc(var(--header-clamp) * 40);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: calc(100% - 2 * var(--header-clamp) * 40);
|
||||
max-width: var(--max-width-content);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: calc(var(--header-clamp) * 24);
|
||||
padding: calc(var(--header-clamp) * 16) calc(var(--header-clamp) * 40);
|
||||
background-color: var(--header-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: var(--heder-border);
|
||||
box-shadow: var(--header-shadow);
|
||||
z-index: 100;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.rankLiveHeaderTitle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--accent);
|
||||
font-size: 24px;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
|
||||
|
||||
.menuLinksDiv {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menuLinksDiv>a,
|
||||
.menuLinksDiv>div>span {
|
||||
padding: 8px 16px;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 1.25;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menuLinksDiv .menu-icon {
|
||||
margin-right: 8px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.menuLinksDiv>div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menuLinksDiv>a::after,
|
||||
.menuLinksDiv>div>span::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 2px;
|
||||
height: 2px;
|
||||
background-color: var(--accent);
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.menuLinksDiv>div>span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.menuLinksDiv>div>span>svg.menu-chevron {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.menuLinksDiv>div>span:hover>svg.menu-chevron,
|
||||
.menuLinksDiv>div.open>span>svg.menu-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menuLinksDiv>a:hover::after,
|
||||
.menuLinksDiv>div>span:hover::after,
|
||||
.menuLinksDiv>div.open>span::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.menuLinksDiv .dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: var(--menu-dropdown-bg);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px var(--menu-box-shadows);
|
||||
padding: 8px 0;
|
||||
display: none;
|
||||
min-width: 160px;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.menuLinksDiv .dropdown::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 15px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.menuLinksDiv .dropdown a {
|
||||
display: block;
|
||||
padding: 8px 16px;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown a:hover, .dropdown form:hover {
|
||||
background-color: var(--dropdown-el-hover);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: var(--header-bg);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.06);
|
||||
padding: calc(var(--header-clamp) * 60) 24px 32px 24px;
|
||||
gap: 8px;
|
||||
width: 288px;
|
||||
}
|
||||
|
||||
.sidebar>a,
|
||||
.sidebar>div>.menu-item {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar>a:hover,
|
||||
.sidebar>a:active,
|
||||
.sidebar>div>.menu-item:hover,
|
||||
.sidebar>div.open>.menu-item {
|
||||
background-color: rgba(0, 0, 0, 0.014);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.sidebar .menu-icon {
|
||||
margin-right: 14px;
|
||||
opacity: 0.85;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar>div>.menu-item>.menu-chevron {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar>div.open>.menu-item>.menu-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.sidebar .dropdown {
|
||||
background-color: var(--sidebar-dropdown);
|
||||
border-radius: 12px;
|
||||
box-shadow: inset 0 2px 8px var(--sidebar-dropdown-shadow);
|
||||
padding: 8px;
|
||||
margin: 4px 16px 8px 16px;
|
||||
display: none;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.sidebar .dropdown a, .sidebar button {
|
||||
display: block;
|
||||
padding: 10px 16px;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar .dropdown a:hover {
|
||||
background-color: var(--sidebar-dropdown-el-hover);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.sidebar a,
|
||||
.sidebar>div {
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
|
||||
.burgerMenuDiv {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: calc(var(--header-clamp) * 30);
|
||||
height: calc(var(--header-clamp) * 21);
|
||||
cursor: pointer;
|
||||
z-index: 99;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.burgerMenuLine {
|
||||
height: calc(var(--header-clamp) * 3);
|
||||
width: 100%;
|
||||
background-color: var(--accent);
|
||||
border-radius: calc(var(--header-clamp) * 3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.burgerMenuDiv.active .burgerMenuLine:nth-child(1) {
|
||||
transform: translateY(calc(var(--header-clamp) * 9)) rotate(45deg);
|
||||
}
|
||||
|
||||
.burgerMenuDiv.active .burgerMenuLine:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.burgerMenuDiv.active .burgerMenuLine:nth-child(3) {
|
||||
transform: translateY(calc(var(--header-clamp) * -9)) rotate(-45deg);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
height: 100vh;
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.sidebar.active {
|
||||
opacity: 1;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
.menuLinksDiv {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 921px) {
|
||||
.burgerMenuDiv {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
:root {
|
||||
--main: #2d2726;
|
||||
--bg: #FDFDFD;
|
||||
--border-subtle: #d4d7e1;
|
||||
--text-main: #191919;
|
||||
--text-muted: #5e5e5e;
|
||||
--paddingSite: 20px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--main: #f0f0f0; /* Flipped to a bright off-white for main accents/highlights */
|
||||
--bg: #121212; /* Dark background (replacing the near-white #FDFDFD) */
|
||||
--border-subtle: #2d2d2d; /* Soft, dark border that won't clip harshly against the background */
|
||||
--text-main: #f5f5f5; /* Light grey/white for high readability */
|
||||
--text-muted: #a0a0a0; /* Mid-tone grey for secondary text */
|
||||
--paddingSite: 20px; /* Kept identical for layout consistency */
|
||||
}
|
||||
}
|
||||
|
||||
/* --- BASE & TYPOGRAPHY --- */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
background: var(--bg);
|
||||
color: var(--text-main);
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: none;
|
||||
font-optical-sizing: auto;
|
||||
}
|
||||
|
||||
.bgSection {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
margin: 100px auto 80px auto;
|
||||
padding: var(--paddingSite);
|
||||
max-width: 1800px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.bgSection {
|
||||
margin-top: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.headerWrapper {
|
||||
margin: 40px 0;
|
||||
color: var(--main);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.headerMain {
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 200;
|
||||
font-size: clamp(34px, 6vw, 3.5rem);
|
||||
letter-spacing: 12px;
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.headerWrapper > h4 {
|
||||
margin: 0 0 10px 0;
|
||||
letter-spacing: 3px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.headerAbt {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
color: var(--main);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.tableCutter {
|
||||
height: 120px;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.tableWraper {
|
||||
margin: 20px 0 80px 0;
|
||||
}
|
||||
|
||||
.customDisplayEditorTable th {
|
||||
top: 120px !important;
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
:root {
|
||||
--main: #2d2726;
|
||||
--bg-top-raw: 254 63 24;
|
||||
--main-box-shadow: rgb(from var(--main) r g b / 0.05);
|
||||
--accent: #fe3f18;
|
||||
--accent-hover: #c5e300;
|
||||
--paddingSite: 40px;
|
||||
--card-radius: 20px;
|
||||
--card-bg: #ffffff;
|
||||
--bg: #FDFDFD;
|
||||
--card-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
|
||||
--border-subtle: #d4d7e1;
|
||||
--text-main: #191919;
|
||||
--text-muted: #5e5e5e;
|
||||
}
|
||||
|
||||
/* --- BASE & TYPOGRAPHY --- */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
background: var(--bg);
|
||||
color: var(--text-main);
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: none;
|
||||
font-optical-sizing: auto;
|
||||
}
|
||||
|
||||
.bgSection {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
padding: var(--paddingSite);
|
||||
max-width: 1400px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.headerAbt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.headerAbt h2 {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
color: var(--main);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.textInputAbtTitel {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--main);
|
||||
font-size: 1.5rem;
|
||||
color: #191919;
|
||||
background: none;
|
||||
width: max-content;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
/* Sidebar Menu Button Styling */
|
||||
.header-text-conatiner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header-text-conatiner button {
|
||||
cursor: auto;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
padding: 12px 20px;
|
||||
border-radius: 100px;
|
||||
background: var(--accent);
|
||||
border: 1px solid transparent;
|
||||
font-size: 15px;
|
||||
color: #000;
|
||||
width: 100%;
|
||||
box-shadow: 0 4px 6px rgba(207, 239, 0, 0.2);
|
||||
}
|
||||
|
||||
.header-text-conatiner button:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(207, 239, 0, 0.3);
|
||||
}
|
||||
|
||||
.header-text-conatiner button:active {
|
||||
transform: scale(0.98) translateY(0);
|
||||
}
|
||||
|
||||
.header-text-conatiner label {
|
||||
font-weight: 400;
|
||||
color: var(--text-main);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
.header-text-conatiner input[type="number"] {
|
||||
padding: 12px 15px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: #fff;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-text-conatiner input[type="number"]:focus {
|
||||
border-color: var(--main);
|
||||
box-shadow: 0 0 0 3px rgba(40, 102, 110, 0.1);
|
||||
}
|
||||
|
||||
.geraet-table {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--card-shadow);
|
||||
min-width: 260px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.geraet-table thead {
|
||||
background: var(--main);
|
||||
}
|
||||
|
||||
.geraet-table thead th {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #fff;
|
||||
padding: 14px 20px;
|
||||
font-weight: 300;
|
||||
font-size: 1.05rem;
|
||||
text-align: left;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.geraet-table thead th i span {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.geraet-table tbody {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for tbody */
|
||||
.geraet-table tbody::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.geraet-table tbody::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.geraet-table tbody::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.geraet-table tbody::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
.turnerin-row {
|
||||
cursor: auto;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease;
|
||||
display: block;
|
||||
border: 1px solid transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.turnerin-row>td {
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: var(--text-main);
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
/* Grouping Row Styling */
|
||||
.group-row {
|
||||
background: transparent;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.group-row>td {
|
||||
padding: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
padding: 12px 16px;
|
||||
cursor: auto;
|
||||
gap: 18px;
|
||||
border-radius: 10px;
|
||||
font-weight: 200;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.group-inner {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.group-row .group-inner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.allGeraeteDiv {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.buttonsDiv {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 20px;
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.buttonsDiv > button {
|
||||
background: var(--main);
|
||||
border: 1px solid #fff;
|
||||
color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.buttonsDiv > select {
|
||||
background: #fff;
|
||||
border: 1px solid var(--main);
|
||||
color: var(--main);
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.buttonsDiv > button:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.buttonsDiv > button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.filtered-out {
|
||||
opacity: 0.5;
|
||||
background: #4d4d4d22 !important;
|
||||
color: #4d4d4d !important;
|
||||
}
|
||||
|
||||
.filtered-out-border {
|
||||
border: 1px solid #4d4d4d !important;
|
||||
}
|
||||
|
||||
.timeSpan {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.stickyDivHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--bg);
|
||||
padding: 10px 0;
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>400 - Bad Request</title>
|
||||
<link rel="icon" type="image/png" href="/intern/img/icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/error-pages/style.css">
|
||||
<script>
|
||||
window.ERROR_CODE = 400;
|
||||
</script>
|
||||
<script src="/error-pages/translations.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<section>
|
||||
<div class="header">
|
||||
<h1 id="title">Ungültige Anfrage</h1>
|
||||
<p id="subtitle">HTTP Fehlercode 400</p>
|
||||
</div>
|
||||
|
||||
<div class="lang-switcher">
|
||||
<select class="lang-select" id="langSelect" onchange="translatePage(this.value)">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="it">Italiano</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="content" id="message">
|
||||
Ihre Anfrage ist fehlerhaft und das gesuchtes Ziel kann nicht gefunden werden.
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>400 - Bad Request</title>
|
||||
<link rel="icon" type="image/png" href="/intern/img/icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/error-pages/style.css">
|
||||
<script>
|
||||
window.ERROR_CODE = 400;
|
||||
</script>
|
||||
<script src="/error-pages/translations.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<section>
|
||||
<div class="header">
|
||||
<h1 id="title">Ungültige Anfrage</h1>
|
||||
<p id="subtitle">HTTP Fehlercode 400</p>
|
||||
</div>
|
||||
|
||||
<div class="lang-switcher">
|
||||
<select class="lang-select" id="langSelect" onchange="translatePage(this.value)">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="it">Italiano</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="phpContent" id="phpMessage">
|
||||
<?= htmlspecialchars($message ?? '') ?>
|
||||
</div>
|
||||
|
||||
<div class="content" id="message">
|
||||
Ihre Anfrage ist fehlerhaft und das gesuchtes Ziel kann nicht gefunden werden.
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>403 - Forbidden</title>
|
||||
<link rel="icon" type="image/png" href="/intern/img/icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/error-pages/style.css">
|
||||
<script>
|
||||
window.ERROR_CODE = 403;
|
||||
</script>
|
||||
<script src="/error-pages/translations.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<section>
|
||||
<div class="header">
|
||||
<h1 id="title">Anfrage verboten</h1>
|
||||
<p id="subtitle">HTTP Fehlercode 403</p>
|
||||
</div>
|
||||
|
||||
<div class="lang-switcher">
|
||||
<select class="lang-select" id="langSelect" onchange="translatePage(this.value)">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="it">Italiano</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="content" id="message">
|
||||
Sie haben keine Berechtigung diesen Inhalt anzuzeigen.
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>429 - Too many Requests</title>
|
||||
<link rel="icon" type="image/png" href="/intern/img/icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/error-pages/style.css">
|
||||
<script>
|
||||
window.ERROR_CODE = 429;
|
||||
</script>
|
||||
<script src="/error-pages/translations.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<section>
|
||||
<div class="header">
|
||||
<h1 id="title">Zu viele Anfragen</h1>
|
||||
<p id="subtitle">HTTP Fehlercode 429</p>
|
||||
</div>
|
||||
|
||||
<div class="lang-switcher">
|
||||
<select class="lang-select" id="langSelect" onchange="translatePage(this.value)">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="it">Italiano</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="content" id="message">
|
||||
Ihr Netzwerk sendet aktuell zu viele Anfragen an diese Webseite, weshalb diese IP-Adresse temporär blockiert wurde. Versuchen Sie die Seite in wenigen Sekunden neu zu laden.
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>500 - Internal Server Error</title>
|
||||
<link rel="icon" type="image/png" href="/intern/img/icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/error-pages/style.css">
|
||||
<script>
|
||||
window.ERROR_CODE = 500;
|
||||
</script>
|
||||
<script src="/error-pages/translations.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<section>
|
||||
<div class="header">
|
||||
<h1 id="title">Kritischer Fehler beim Laden der Seite</h1>
|
||||
<p id="subtitle">HTTP Fehlercode 500</p>
|
||||
</div>
|
||||
|
||||
<div class="lang-switcher">
|
||||
<select class="lang-select" id="langSelect" onchange="translatePage(this.value)">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="it">Italiano</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="content" id="message">
|
||||
Diese Seite konnte aufgrund von Problemen auf dem Server nicht geladen werden. Bitte versuchen Sie es später erneut. Die Behebung dieses Fehlers kann einige Zeit in Anspruch nehmen.
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,104 @@
|
||||
/* Modern reset and centering */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
color: #1f2937;
|
||||
min-height: 100vh;
|
||||
padding: 20vh 60px;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
body {
|
||||
padding: 15vh 10px;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 32px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Header section with warning color */
|
||||
.header h1 {
|
||||
font-size: 48px;
|
||||
letter-spacing: 2px;
|
||||
color: #36454F;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.05em;
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Sleek Language Dropdown Container */
|
||||
.lang-switcher {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.lang-select {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
padding: 6px 32px 6px 12px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
/* Removes native browser arrow */
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 16px;
|
||||
transition: border-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.lang-select:hover,
|
||||
.lang-select:focus {
|
||||
border-color: #01CB8E;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Divider line */
|
||||
.header::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: min(120px, 50vw);
|
||||
height: 3px;
|
||||
background-color: #01CB8E;
|
||||
margin: 0 0 24px 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* PHP Text */
|
||||
.phpContent {
|
||||
font-size: 24px;
|
||||
line-height: 1.6;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
/* Main text */
|
||||
.content {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #4b5563a2;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
const translations = {
|
||||
429: {
|
||||
de: {
|
||||
title: "Zu viele Anfragen",
|
||||
subtitle: "HTTP Fehlercode 429",
|
||||
message: "Ihr Netzwerk sendet aktuell zu viele Anfragen an diese Webseite, weshalb diese IP-Adresse temporär blockiert wurde. Versuchen Sie die Seite in wenigen Sekunden neu zu laden."
|
||||
},
|
||||
en: {
|
||||
title: "Too Many Requests",
|
||||
subtitle: "HTTP Error Code 429",
|
||||
message: "Your network is currently sending too many requests to this website, which is why this IP address has been temporarily blocked. Please try reloading the page in a few seconds."
|
||||
},
|
||||
fr: {
|
||||
title: "Trop de requêtes",
|
||||
subtitle: "Code d'erreur HTTP 429",
|
||||
message: "Votre réseau envoie actuellement trop de requêtes à ce site web, c'est pourquoi cette adresse IP a été temporairement bloquée. Veuillez réessayer de charger la page dans quelques secondes."
|
||||
},
|
||||
it: {
|
||||
title: "Troppe richieste",
|
||||
subtitle: "Codice di errore HTTP 429",
|
||||
message: "La tua rete sta attualmente inviando troppe richieste a questo sito web, motivo per cui questo indirizzo IP è stato temporaneamente bloccato. Prova a ricaricare la pagina tra pochi secondi."
|
||||
}
|
||||
},
|
||||
403: {
|
||||
de: {
|
||||
title: "Anfrage verboten",
|
||||
subtitle: "HTTP Fehlercode 403",
|
||||
message: "Sie haben keine Berechtigung diesen Inhalt anzuzeigen."
|
||||
},
|
||||
en: {
|
||||
title: "Forbidden",
|
||||
subtitle: "HTTP Error Code 403",
|
||||
message: "You do not have permission to view this content."
|
||||
},
|
||||
fr: {
|
||||
title: "Accès interdit",
|
||||
subtitle: "Code d'erreur HTTP 403",
|
||||
message: "Vous n'avez pas l'autorisation d'afficher ce contenu."
|
||||
},
|
||||
it: {
|
||||
title: "Accesso vietato",
|
||||
subtitle: "Codice di errore HTTP 403",
|
||||
message: "Non hai il permesso di visualizzare questo contenuto."
|
||||
}
|
||||
},
|
||||
400: {
|
||||
de: {
|
||||
title: "Ungültige Anfrage",
|
||||
subtitle: "HTTP Fehlercode 400",
|
||||
message: "Ihre Anfrage ist fehlerhaft und das gesuchte Ziel kann nicht gefunden werden."
|
||||
},
|
||||
en: {
|
||||
title: "Bad Request",
|
||||
subtitle: "HTTP Error Code 400",
|
||||
message: "Your request is malformed and the destination you are looking for cannot be found."
|
||||
},
|
||||
fr: {
|
||||
title: "Requête incorrecte",
|
||||
subtitle: "Code d'erreur HTTP 400",
|
||||
message: "Votre requête est incorrecte et la destination recherchée est introuvable."
|
||||
},
|
||||
it: {
|
||||
title: "Richiesta errata",
|
||||
subtitle: "Codice di errore HTTP 400",
|
||||
message: "La tua richiesta è errata e la destinazione cercata non è rintracciabile."
|
||||
}
|
||||
},
|
||||
500: {
|
||||
de: {
|
||||
title: "Kritischer Fehler beim Laden der Seite",
|
||||
subtitle: "HTTP Fehlercode 500",
|
||||
message: "Diese Seite konnte aufgrund von Problemen auf dem Server nicht geladen werden. Bitte versuchen Sie es später erneut. Die Behebung dieses Fehlers kann einige Zeit in Anspruch nehmen."
|
||||
},
|
||||
en: {
|
||||
title: "Critical Error Loading Page",
|
||||
subtitle: "HTTP Error Code 500",
|
||||
message: "This page could not be loaded due to problems on the server. Please try again later. Resolving this error may take some time."
|
||||
},
|
||||
fr: {
|
||||
title: "Erreur critique lors du chargement de la page",
|
||||
subtitle: "Code d'erreur HTTP 500",
|
||||
message: "Cette page n'a pas pu être chargée en raison de problèmes sur le serveur. Veuillez réessayer plus tard. La résolution de cette erreur peut prendre un certain temps."
|
||||
},
|
||||
it: {
|
||||
title: "Errore critico durante il caricamento della pagina",
|
||||
subtitle: "Codice di errore HTTP 500",
|
||||
message: "Impossibile caricare la pagina a causa di problemi sul server. Si prega di riprovare più tardi. La risoluzione di questo errore potrebbe richiedere del tempo."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const errortype = window.ERROR_CODE;
|
||||
|
||||
function translatePage(lang) {
|
||||
document.documentElement.lang = lang;
|
||||
|
||||
document.getElementById('title').textContent = translations[errortype][lang].title;
|
||||
document.getElementById('subtitle').textContent = translations[errortype][lang].subtitle;
|
||||
document.getElementById('message').textContent = translations[errortype][lang].message;
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const browserLang = navigator.language || navigator.userLanguage;
|
||||
|
||||
const shortLang = browserLang.toLowerCase().split('-')[0];
|
||||
|
||||
const supportedLangs = ['de', 'en', 'fr', 'it'];
|
||||
|
||||
if (supportedLangs.includes(shortLang)) {
|
||||
const selectEl = document.getElementById('langSelect');
|
||||
selectEl.value = shortLang; // Changes the dropdown visual state
|
||||
translatePage(shortLang); // Runs your translation function
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
RewriteEngine On
|
||||
|
||||
# ----------------------------------------
|
||||
# 1) Root → router.php
|
||||
# ----------------------------------------
|
||||
RewriteRule ^$ router.php [L]
|
||||
|
||||
# ----------------------------------------
|
||||
# 2) Allow existing files
|
||||
# ----------------------------------------
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# ----------------------------------------
|
||||
# 3) Allow existing directories
|
||||
# ----------------------------------------
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# ----------------------------------------
|
||||
# 4) Everything else → router.php
|
||||
# ----------------------------------------
|
||||
RewriteRule ^ router.php [QSA,L]
|
||||
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
if (!isset($segments) || !defined('APP_ROUTER')) {
|
||||
http_response_code(403);
|
||||
include $baseDir . "/error-pages/403.php";
|
||||
exit;
|
||||
}
|
||||
|
||||
$type = $segments[1] ?? null;
|
||||
$requested_geraet = htmlspecialchars($segments[2] ?? '');
|
||||
|
||||
$allowed_types = ["audioplayer"];
|
||||
|
||||
if (!in_array($type, $allowed_types)) {
|
||||
http_response_code(400);
|
||||
include $baseDir . "/error-pages/400.html";
|
||||
exit;
|
||||
}
|
||||
|
||||
$baseDir = $_SERVER['DOCUMENT_ROOT'];
|
||||
|
||||
require_once $baseDir . '/../scripts/session_functions.php';
|
||||
|
||||
ini_wkvs_session(true);
|
||||
|
||||
$csrf_token = $_SESSION['csrf_token'] ?? '';
|
||||
|
||||
require $baseDir . '/../scripts/websocket/ws-create-token.php';
|
||||
|
||||
$WSaccesstype = "audio";
|
||||
$WSaccessPermission = "R";
|
||||
|
||||
require_once $baseDir . '/../scripts/db/db-verbindung-script-guest.php';
|
||||
require_once $baseDir . '/../scripts/db/db-functions.php';
|
||||
require_once $baseDir . '/../scripts/db/db-tables.php';
|
||||
|
||||
$stmt = $guest->prepare("SELECT `name`, `id` FROM $db_tabelle_disziplinen WHERE `audiofile` = '1' ORDER BY start_index ASC");
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $stmt->get_result();
|
||||
$disciplines = $result->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
$lowerDisciplines = array_change_key_case(
|
||||
array_column($disciplines, 'id', 'name'),
|
||||
CASE_LOWER
|
||||
);
|
||||
|
||||
$disciplines_indexed = array_column($disciplines, 'name', 'id');
|
||||
|
||||
|
||||
if (!array_key_exists($requested_geraet, $lowerDisciplines)) {
|
||||
http_response_code(400);
|
||||
$message = 'Das gesuchte Gerät kann nicht gefunden werden';
|
||||
include $baseDir . "/error-pages/400.php";
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($lowerDisciplines[$requested_geraet])) {
|
||||
http_response_code(500);
|
||||
include $baseDir . "/error-pages/500.html";
|
||||
exit;
|
||||
}
|
||||
|
||||
$geraetId = $lowerDisciplines[$requested_geraet];
|
||||
|
||||
$WSaccess = (string) $geraetId;
|
||||
|
||||
$jsonUrl = '/externe-geraete/json/audio-id-' . $geraetId . '.json';
|
||||
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Remote Audioplayer</title>
|
||||
<meta name="robots" content="noindex">
|
||||
<script src="/intern/js/jquery/jquery-3.7.1.min.js"></script>
|
||||
<script src="/intern/js/custom-msg-display.js"></script>
|
||||
<link rel="stylesheet" href="/intern/css/custom-msg-display.css">
|
||||
<link rel="stylesheet" href="/externe-geraete/css/audio.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<section>
|
||||
<div class="header">
|
||||
<h1 id="title">Audioplayer</h1>
|
||||
<p id="subtitle"><?= $disciplines_indexed[$geraetId] ?></p>
|
||||
</div>
|
||||
|
||||
<button class="startButton">
|
||||
Audioplayer starten
|
||||
</button>
|
||||
|
||||
<div class="currentyPlaying hidden">
|
||||
Aktuell laufendes Audio:
|
||||
<div class="subDivCurrentyPlaying">
|
||||
<span>Dateiname: <span class="spanFilename"></span></span>
|
||||
<span class="spanPersonname">Name der Person:
|
||||
<span class="spanPersonnameName"></span>,
|
||||
<span class="spanPersonnameVorname"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="progressDiv">
|
||||
<div id="progressContainer">
|
||||
<div id="progressBar"></div>
|
||||
</div>
|
||||
|
||||
<span class="progressText"><span id="currentText">0:00</span> / <span id="durationText">0:00</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
Dies ist der Live Audioplayer. Audio kann über die Kampfrichteransicht gestartet werden.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<audio id="peep" preload="auto"></audio>
|
||||
<audio id="musicPlayer" preload="auto"></audio>
|
||||
<script>
|
||||
const WSaccesstype = "<?= $WSaccesstype ?>";
|
||||
const WSaccess = "<?= $WSaccess ?>";
|
||||
const csrf_token = "<?= $csrf_token ?>";
|
||||
const WSaccessPermission = "<?= $WSaccessPermission ?>"
|
||||
|
||||
const jsonUrl = "<?php echo $jsonUrl; ?>";
|
||||
|
||||
let ws;
|
||||
|
||||
let firstConnect = true;
|
||||
const RETRY_DELAY = 2000;
|
||||
|
||||
const urlAjaxNewWSToken = '/intern/scripts/ajax-create-ws-token.php';
|
||||
|
||||
async function fetchNewWSToken(accesstype, access, accessPermission) {
|
||||
try {
|
||||
const response = await fetch(urlAjaxNewWSToken, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
csrf_token,
|
||||
accesstype,
|
||||
access,
|
||||
accessPermission
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const data = await response.json();
|
||||
return data.success ? data.token : null;
|
||||
} catch (error) {
|
||||
console.error("Token fetch failed:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function startWebSocket() {
|
||||
console.log("Attempting WebSocket connection...");
|
||||
|
||||
let token;
|
||||
if (firstConnect) {
|
||||
token = '<?= generateWSReadToken($WSaccesstype, $WSaccess) ?>';
|
||||
} else {
|
||||
token = await fetchNewWSToken(WSaccesstype, WSaccess, WSaccessPermission);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.error("No valid token available. Retrying...");
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ws = new WebSocket(`wss://${window.location.hostname}/ws/?access=token&token=${token}`);
|
||||
} catch (err) {
|
||||
console.error("Malformed WebSocket URL", err);
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket connected!");
|
||||
if (!firstConnect) {
|
||||
displayMsg(1, "Live Syncronisation wieder verfügbar");
|
||||
}
|
||||
firstConnect = true;
|
||||
};
|
||||
|
||||
ws.onerror = (event) => {
|
||||
console.error("WebSocket error observed.");
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
displayMsg(0, "Live Syncronisation verloren");
|
||||
firstConnect = false;
|
||||
|
||||
scheduleRetry();
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleRetry() {
|
||||
console.log(`Retrying in ${RETRY_DELAY}ms...`);
|
||||
setTimeout(startWebSocket, RETRY_DELAY);
|
||||
}
|
||||
|
||||
startWebSocket();
|
||||
|
||||
const spanFilename = $('.spanFilename');
|
||||
const spanPersonname = $('.spanPersonname');
|
||||
const spanPersonnameName = $('.spanPersonnameName');
|
||||
const spanPersonnameVorname = $('.spanPersonnameVorname');
|
||||
|
||||
const currentyPlaying = $('.currentyPlaying');
|
||||
|
||||
const $progressBar = $('#progressBar');
|
||||
const $progressContainer = $('#progressContainer');
|
||||
|
||||
const peep = document.getElementById('peep');
|
||||
const music = document.getElementById('musicPlayer');
|
||||
const $audio = $('#musicPlayer');
|
||||
|
||||
let lastMusicUrl = null;
|
||||
let pollingStarted = false; // ensure we start polling only once
|
||||
|
||||
async function fetchAndHandleMusic() {
|
||||
try {
|
||||
const response = await fetch(jsonUrl + '?t=' + Date.now(), { cache: 'no-store' });
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.audio_path || data.audio_path === 'nan' || data.start == false) {
|
||||
if (!music.paused) {
|
||||
music.pause();
|
||||
music.currentTime = 0;
|
||||
}
|
||||
if (!peep.paused) {
|
||||
peep.pause();
|
||||
peep.currentTime = 0;
|
||||
}
|
||||
lastMusicUrl = null;
|
||||
return;
|
||||
}
|
||||
|
||||
peep.src = '/files/music/piep.mp3';
|
||||
music.src = data.audio_path;
|
||||
music.currentTime = 0;
|
||||
peep.play().then(() => {
|
||||
setTimeout(() => {
|
||||
music.play().catch(err => console.log('Music play error:', err));
|
||||
}, 2000);
|
||||
}).catch(err => console.log('Peep play error:', err));
|
||||
|
||||
spanFilename.text(data.audio_name ?? '');
|
||||
spanPersonnameName.text(data.person.name ?? '');
|
||||
spanPersonnameVorname.text(data.person.vorname ?? '');
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error fetching JSON:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (isNaN(seconds)) return "0:00";
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return mins + ":" + (secs < 10 ? '0' : '') + secs;
|
||||
}
|
||||
|
||||
const audioDOM = $audio[0];
|
||||
|
||||
$audio.on('timeupdate', function() {
|
||||
if (audioDOM.duration) {
|
||||
const percentage = (audioDOM.currentTime / audioDOM.duration) * 100;
|
||||
$progressBar.css('width', percentage + '%');
|
||||
$('#currentText').text(formatTime(audioDOM.currentTime));
|
||||
}
|
||||
});
|
||||
|
||||
$audio.on('loadedmetadata', function() {
|
||||
$('#durationText').text(formatTime(audioDOM.duration));
|
||||
});
|
||||
|
||||
let isActive = false;
|
||||
|
||||
$('.startButton').on('click', function() {
|
||||
isActive = !isActive;
|
||||
if (isActive) {
|
||||
$(this).text('Audioplayer stoppen');
|
||||
} else {
|
||||
$(this).text('Audioplayer starten');
|
||||
currentyPlaying.addClass('hidden');
|
||||
if (!music.paused) {
|
||||
music.pause();
|
||||
music.currentTime = 0;
|
||||
}
|
||||
if (!peep.paused) {
|
||||
peep.pause();
|
||||
peep.currentTime = 0;
|
||||
}
|
||||
}
|
||||
$('section').toggleClass('playerActive', isActive);
|
||||
|
||||
|
||||
if (!pollingStarted) {
|
||||
pollingStarted = true;
|
||||
ws.addEventListener("message", () => {
|
||||
if (isActive) {
|
||||
fetchAndHandleMusic();
|
||||
currentyPlaying.removeClass('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,154 @@
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
color: #1f2937;
|
||||
min-height: 100vh;
|
||||
padding: 20vh 60px;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
body {
|
||||
padding: 15vh 10px;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 32px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 48px;
|
||||
letter-spacing: 2px;
|
||||
color: #36454F;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.05em;
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
|
||||
.startButton {
|
||||
font-family: inherit;
|
||||
font-size: 28px;
|
||||
color: #4b5563;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px 6px 12px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
transition: border-color 0.15s ease, color 0.15s ease;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.startButton:hover,
|
||||
.startButton:focus {
|
||||
border-color: #01CB8E;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
section.playerActive .startButton:hover,
|
||||
section.playerActive .startButton:focus {
|
||||
border-color: #cb0198;
|
||||
}
|
||||
|
||||
.header::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: min(240px, 50vw);
|
||||
height: 8px;
|
||||
background-color: #cb0198;
|
||||
margin: 0 0 24px 0;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
section.playerActive .header::after {
|
||||
background-color: #01CB8E;
|
||||
}
|
||||
|
||||
.currentyPlaying {
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
color: #4b5563;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.subDivCurrentyPlaying {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.spanPersonnameName {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subDivCurrentyPlaying {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #4b5563a2;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#progressContainer {
|
||||
width: 100%;
|
||||
background: #e0e0e0;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#progressBar {
|
||||
width: 0%;
|
||||
background: #2196F3;
|
||||
height: 100%;
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
.progressDiv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
width: min(100%, 300px);
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
border: 1px solid #a3a8af;
|
||||
border-radius: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.progressText {
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
:root {
|
||||
--display-bg: #111827;
|
||||
--display-panel: #020617;
|
||||
--display-panel-soft: #0b1120;
|
||||
--display-accent: #38bdf8;
|
||||
--display-accent-soft: rgba(56, 189, 248, 0.15);
|
||||
--display-text-main: #e5e7eb;
|
||||
--display-text-muted: #9ca3af;
|
||||
--display-danger: #ef4444;
|
||||
--display-success: #22c55e;
|
||||
--display-shadow: #0f172ad9;
|
||||
--display-radius-lg: 18px;
|
||||
--display-border: #94a3b859;
|
||||
--display-transition-fast: 180ms ease-out;
|
||||
--display-colorStartDiv: #0b1120;
|
||||
--display-panelBgLogo: #4a2f96;
|
||||
--display-font-heading: clamp(1.5rem, 6vh, 4rem);
|
||||
--display-font-sub: clamp(1rem, 3.5vh, 2.2rem);
|
||||
--display-logo-size: clamp(150px, 40vh, 500px);
|
||||
--display-error-WS-text: #a7e1fa;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
head>* {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.displayDiv {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.displayDiv {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background-color: var(--display-bg);
|
||||
color: var(--display-text-main);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* WebSocket error overlay */
|
||||
.displayDiv.errorws {
|
||||
position: relative;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 0;
|
||||
background: radial-gradient(circle at top, #020617 0, #020617 55%);
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.displayDiv.errorws svg {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
top: 20vh;
|
||||
}
|
||||
|
||||
.displayDiv.errorws .logoimg {
|
||||
max-width: var(--display-logo-size);
|
||||
height: auto;
|
||||
margin-bottom: 2vh;
|
||||
filter: drop-shadow(0 12px 30px rgba(0, 0, 0, 0.65));
|
||||
}
|
||||
|
||||
.divErrorTextWs {
|
||||
position: absolute;
|
||||
bottom: 5vh;
|
||||
}
|
||||
|
||||
.errortext {
|
||||
margin: 0;
|
||||
font-size: clamp(1.8rem, 8vw, 5rem);
|
||||
line-height: 1.1;
|
||||
max-width: 90vw;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--display-error-WS-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.errortextSmall {
|
||||
margin: 10px;
|
||||
font-size: clamp(0.5rem, 3vw, 2rem);
|
||||
opacity: 0.8;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--display-error-WS-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Startup logo overlay */
|
||||
.logobg {
|
||||
padding: 4vh;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background-color: var(--display-panelBgLogo);
|
||||
z-index: 1000;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.logoimg {
|
||||
width: auto;
|
||||
max-height: min(45vh, 100%);
|
||||
max-width: min(80vw, 100%);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logotext {
|
||||
margin: 0;
|
||||
font-size: clamp(1.8rem, 7vh, 6rem);
|
||||
text-align: center;
|
||||
width: 90vw;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logotext,
|
||||
.logoctext {
|
||||
color: var(--display-text-color) !important;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
margin-top: 1vh;
|
||||
}
|
||||
|
||||
.logoctext {
|
||||
margin: calc(clamp(2rem, 7vw, 12rem) / calc(100% / 10)) 0;
|
||||
font-size: clamp(2rem, 7vw, 12rem);
|
||||
}
|
||||
|
||||
/* 4. Responsive adjustments for different screen ratios */
|
||||
@media (aspect-ratio: 4/3),
|
||||
(aspect-ratio: 10/7) {
|
||||
|
||||
/* Tablet specific tweaks (iPad/Android Tabs) */
|
||||
:root {
|
||||
--display-logo-size: 30vh;
|
||||
}
|
||||
|
||||
.logotext {
|
||||
font-size: 5vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1921px) {
|
||||
|
||||
/* Ultra-large TV/4K tweaks */
|
||||
.logotext {
|
||||
letter-spacing: 0.25em;
|
||||
/* Better legibility at distance */
|
||||
}
|
||||
}
|
||||
|
||||
/* Main scoreboard container – full screen */
|
||||
.pagediv {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2vh;
|
||||
padding: 2.2vh 2.4vw;
|
||||
box-sizing: border-box;
|
||||
background:
|
||||
radial-gradient(circle at top left, var(--display-bg-soft), transparent 60%),
|
||||
radial-gradient(circle at bottom right, var(--display-bg-soft), transparent 60%);
|
||||
}
|
||||
|
||||
.pagediv.manuel {
|
||||
padding-right: clamp(60px, 2.4vw, 2.4vw);
|
||||
}
|
||||
|
||||
/* Common row styling */
|
||||
.display-row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30px;
|
||||
border-radius: var(--display-radius-lg);
|
||||
background: linear-gradient(135deg, var(--display-panel-soft), var(--display-panel));
|
||||
border-bottom: 1px solid var(--display-border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 45px var(--display-shadow);
|
||||
}
|
||||
|
||||
.display-row::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top left, var(--display-bg-soft), transparent 55%);
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.row1text,
|
||||
.row2text p,
|
||||
.row3>p,
|
||||
.start_text {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.1;
|
||||
align-self: start;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Row 1: athlete name */
|
||||
.row1text {
|
||||
font-size: min(8vh, 4.6vw);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Row 2: club, program, start/stop */
|
||||
.row2 {
|
||||
justify-content: space-between;
|
||||
gap: 2vw;
|
||||
}
|
||||
|
||||
.row2::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at bottom right, var(--display-colorStartDiv), transparent 55%);
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.row2text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8vh;
|
||||
width: 50vw;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.row2_1text {
|
||||
color: var(--display-text-muted);
|
||||
}
|
||||
|
||||
.row2_2text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Start / stop pill */
|
||||
.start_div {
|
||||
width: 35vw;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.start_text {
|
||||
display: flex;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
/* Row 3: D-score and total */
|
||||
.row3 {
|
||||
justify-content: space-between;
|
||||
gap: 2vw;
|
||||
}
|
||||
|
||||
.row3>p {
|
||||
font-size: min(9vh, 5vw);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.row3_1text {
|
||||
color: var(--display-color-note-l);
|
||||
}
|
||||
|
||||
.row3_2text {
|
||||
color: var(--display-color-note-r);
|
||||
}
|
||||
|
||||
/* Existing #score and char/row styles if you use them elsewhere */
|
||||
#score {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.char {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 40vw;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: 20vw;
|
||||
}
|
||||
|
||||
/* Landscape / portrait logo scaling */
|
||||
@media (orientation: landscape) {
|
||||
.logoimg {
|
||||
max-height: min(60vh, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: portrait) {
|
||||
.logoimg {
|
||||
max-width: min(60vw, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Small screens – keep everything readable */
|
||||
@media (max-width: 768px) {
|
||||
.pagediv {
|
||||
padding: 1.6vh 3vw;
|
||||
gap: 1.4vh;
|
||||
}
|
||||
|
||||
.display-row {
|
||||
padding: 1.4vh 3vw;
|
||||
}
|
||||
|
||||
.row2 {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.start_div {
|
||||
align-self: flex-end;
|
||||
min-width: 40vw;
|
||||
}
|
||||
}
|
||||
|
||||
.noWsConnection {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
|
||||
transform: rotate(270deg);
|
||||
transform-origin: right bottom;
|
||||
}
|
||||
|
||||
.rotator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.noWsConnection img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.noWsConnection p {
|
||||
font-size: 20px;
|
||||
color: var(--display-text-muted);
|
||||
margin: 0;
|
||||
letter-spacing: 1.1px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ctext .cTextHidden {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -0,0 +1,814 @@
|
||||
<?php
|
||||
|
||||
if (!isset($segments) || !defined('APP_ROUTER')) {
|
||||
http_response_code(403);
|
||||
include $baseDir . "/error-pages/403.php";
|
||||
exit;
|
||||
}
|
||||
|
||||
$type = $segments[1] ?? null;
|
||||
$requested_geraet = htmlspecialchars($segments[2] ?? '');
|
||||
|
||||
$allowed_types = ["display"];
|
||||
|
||||
if (!in_array($type, $allowed_types)) {
|
||||
http_response_code(400);
|
||||
include $baseDir . "/error-pages/400.html";
|
||||
exit;
|
||||
}
|
||||
|
||||
$baseDir = $_SERVER['DOCUMENT_ROOT'];
|
||||
|
||||
require_once $baseDir . '/../scripts/session_functions.php';
|
||||
|
||||
ini_wkvs_session(true);
|
||||
|
||||
$csrf_token = $_SESSION['csrf_token'] ?? '';
|
||||
|
||||
require $baseDir . '/../scripts/websocket/ws-create-token.php';
|
||||
|
||||
$WSaccesstype = "display";
|
||||
$WSaccessPermission = "R";
|
||||
|
||||
require_once $baseDir . '/../scripts/db/db-verbindung-script-guest.php';
|
||||
require_once $baseDir . '/../scripts/db/db-functions.php';
|
||||
require_once $baseDir . '/../scripts/db/db-tables.php';
|
||||
|
||||
$stmt = $guest->prepare("SELECT `name`, `id` FROM $db_tabelle_disziplinen ORDER BY start_index ASC");
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $stmt->get_result();
|
||||
$disciplines = $result->fetch_all(MYSQLI_ASSOC);
|
||||
$lowerDisciplines = array_change_key_case(
|
||||
array_column($disciplines, 'id', 'name'),
|
||||
CASE_LOWER
|
||||
);
|
||||
|
||||
|
||||
if (!array_key_exists($requested_geraet, $lowerDisciplines)) {
|
||||
http_response_code(400);
|
||||
$message = 'Das gesuchte Gerät kann nicht gefunden werden';
|
||||
include $baseDir . "/error-pages/400.php";
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($lowerDisciplines[$requested_geraet])) {
|
||||
http_response_code(500);
|
||||
include $baseDir . "/error-pages/500.html";
|
||||
exit;
|
||||
}
|
||||
|
||||
$geraetId = $lowerDisciplines[$requested_geraet];
|
||||
|
||||
$WSaccess = (string) $geraetId;
|
||||
|
||||
|
||||
$stmt->close();
|
||||
|
||||
// Define a small helper function to keep the code DRY (Don't Repeat Yourself)
|
||||
function sanitize_hex($color) {
|
||||
return preg_replace('/[^0-9a-fA-F#]/', '', $color);
|
||||
}
|
||||
|
||||
$keysNeeded = [
|
||||
'wkName', 'displayColourLogo', 'displayTextColourLogo', 'displayColorScoringBg',
|
||||
'displayColorScoringBgSoft', 'displayColorScoringPanel', 'displayColorScoringPanelSoft', 'displayColorScoringBorderColor',
|
||||
'displayColorScoringShadowColor', 'displayColorScoringPanelText', 'displayColorScoringPanelTextSoft',
|
||||
'displayColorScoringPanelTextNoteL', 'displayColorScoringPanelTextNoteR', 'displayIdNoteL',
|
||||
'displayIdNoteR', 'displayCTextLogo', 'displayCTextWKName'
|
||||
];
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($keysNeeded), '?'));
|
||||
|
||||
$query = "SELECT `name`, `value` FROM $db_tabelle_var WHERE `name` IN ($placeholders)";
|
||||
|
||||
$stmt = $guest->prepare($query);
|
||||
$params = str_repeat("s", count($keysNeeded));
|
||||
$stmt->bind_param($params, ...$keysNeeded);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
$config = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$config[$row['name']] = $row['value'];
|
||||
}
|
||||
|
||||
$wkName = $config['wkName'] ?? null;
|
||||
$displayCTextDisplayLogo = $config['displayCTextLogo'] ?? null;
|
||||
$displayCTextDisplayWKName = $config['displayCTextWKName'] ?? null;
|
||||
$cleanColor = $config['displayColourLogo'] ?? null;
|
||||
$cleanColorText = $config['displayTextColourLogo'] ?? null;
|
||||
$displayColorScoringBg = $config['displayColorScoringBg'] ?? null;
|
||||
$displayColorScoringBgSoft = $config['displayColorScoringBgSoft'] ?? null;
|
||||
$displayColorScoringPanel = $config['displayColorScoringPanel'] ?? null;
|
||||
$displayColorScoringPanelSoft = $config['displayColorScoringPanelSoft'] ?? null;
|
||||
$displayColorScoringShadowColor = $config['displayColorScoringShadowColor'] ?? null;
|
||||
$displayColorScoringBorderColor = $config['displayColorScoringBorderColor'] ?? null;
|
||||
$displayColorScoringPanelText = $config['displayColorScoringPanelText'] ?? null;
|
||||
$displayColorScoringPanelTextSoft = $config['displayColorScoringPanelTextSoft'] ?? null;
|
||||
$displayColorScoringPanelTextNoteL = $config['displayColorScoringPanelTextNoteL'] ?? null;
|
||||
$displayColorScoringPanelTextNoteR = $config['displayColorScoringPanelTextNoteR'] ?? null;
|
||||
|
||||
$stmt->close();
|
||||
|
||||
$guest->close();
|
||||
|
||||
$jsonUrlconfig = '/externe-geraete/json/config.json';
|
||||
$jsonUrl = '/externe-geraete/json/display-id-' . $geraetId . '.json';
|
||||
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><?= $wkName ?> Anzeigen</title>
|
||||
<meta name="robots" content="noindex">
|
||||
<link rel="stylesheet" href="/externe-geraete/css/display.css">
|
||||
<script src="/intern/js/gsap/gsap.min.js"></script>
|
||||
<script src="/intern/js/gsap/MorphSVGPlugin.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
|
||||
/* Brand */
|
||||
--display-panelBgLogo: <?= $cleanColor ?>;
|
||||
--display-text-color: <?= $cleanColorText ?>;
|
||||
|
||||
/* Backgrounds */
|
||||
--display-bg: <?= $displayColorScoringBg ?>;
|
||||
--display-bg-soft: <?= $displayColorScoringBgSoft ?>;
|
||||
--display-panel: <?= $displayColorScoringPanel ?>;
|
||||
--display-panel-soft: <?= $displayColorScoringPanelSoft ?>;
|
||||
|
||||
/* Shadow */
|
||||
--display-shadow: <?= $displayColorScoringShadowColor ?>;
|
||||
|
||||
/* Border */
|
||||
--display-border: <?= $displayColorScoringBorderColor ?>;
|
||||
|
||||
/* Typography */
|
||||
--display-text-main: <?= $displayColorScoringPanelText ?>;
|
||||
--display-text-muted: <?= $displayColorScoringPanelTextSoft ?>;
|
||||
|
||||
/* Noten */
|
||||
--display-color-note-l: <?= $displayColorScoringPanelTextNoteL ?>;
|
||||
--display-color-note-r: <?= $displayColorScoringPanelTextNoteR ?>;
|
||||
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="displayDiv">
|
||||
<div class="errorws">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="errorSvg" viewBox="0 0 800 200">
|
||||
<defs>
|
||||
<filter id="a" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="8" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<linearGradient id="myLinearGradient" x1="0%" y1="5%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#22c55e00" />
|
||||
<stop offset="100%" stop-color="#22c55e90" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M150 100h200m100 0h200" id="errorLine" opacity=".3" stroke="#3b82f6" stroke-width="4" stroke-dasharray="10,15" stroke-linecap="round"/>
|
||||
<g transform="translate(150 100)">
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
|
||||
<rect x="-30" y="-19" width="60" height="38" rx="2" fill="#334155"/>
|
||||
<rect x="-28" y="-17" width="56" height="10" rx="2" fill="#555"/>
|
||||
<rect x="-28" y="-5" width="56" height="10" rx="2" fill="#555"/>
|
||||
<rect x="-28" y="7" width="56" height="10" rx="2" fill="#555"/>
|
||||
<text x="-26" y="-11" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">Test WKVS</text>
|
||||
|
||||
<text x="-26" y="-1" fill="#94a3b8" font-size="2.5" font-family="monospace" letter-spacing="0">WKVS,</text>
|
||||
<text x="-26" y="2.5" fill="#94a3b8" font-size="2" font-family="monospace" letter-spacing="0">EP</text>
|
||||
<rect x="0" y="-5" width="28" rx="2" height="10" fill="url(#myLinearGradient)" />
|
||||
|
||||
<text x="10" y="1" fill="#22c55e" font-size="4" font-family="monospace" letter-spacing="0.5">START</text>
|
||||
|
||||
<text x="-26" y="13" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">D 1.40</text>
|
||||
<text x="15" y="13" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">9.40</text>
|
||||
|
||||
|
||||
|
||||
<text y="70" text-anchor="middle" fill="#94a3b8" font-size="12" font-family="monospace" letter-spacing="1.5">Anzeige</text>
|
||||
</g>
|
||||
<g transform="translate(650 100)">
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
<rect x="-18" y="-15" width="36" height="10" rx="2" fill="#334155"/>
|
||||
<rect x="-18" y="2" width="36" height="10" rx="2" fill="#334155"/>
|
||||
<circle cx="10" cy="-10" r="2" fill="#10b981"/>
|
||||
<circle cx="10" cy="7" r="2" fill="#10b981"/>
|
||||
<text y="70" text-anchor="middle" fill="#94a3b8" font-size="12" font-family="monospace" letter-spacing="1.5">WKVS Server</text>
|
||||
</g>
|
||||
<path fill="#f43f5e" class="opacity0gsap" opacity=".4" d="m-14.142-70.71 5.657 5.656-5.657 5.657-5.657-5.657z" transform="translate(400 100)"/>
|
||||
<path fill="#f43f5e" class="opacity0gsap" opacity=".4" d="m42.67 39.74 5.869 1.247-1.248 5.87-5.868-1.248z" transform="translate(400 100)"/>
|
||||
<path fill="#3b82f6" class="opacity0gsap" opacity=".4" d="m-50.98 28.301 8.66 5-5 8.66-8.66-5z" transform="translate(400 100)"/>
|
||||
<g filter="url(#a)" transform="translate(400 100)">
|
||||
<path id="cross"
|
||||
d="M-30,-30 L30,30 M30,-30 L-30,30"
|
||||
stroke="#f43f5e"
|
||||
stroke-width="8"
|
||||
stroke-linecap="round"
|
||||
fill="none"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="okSvg" viewBox="0 0 800 200" style="display: none">
|
||||
<defs>
|
||||
<filter id="a" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="8" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<linearGradient id="myLinearGradient" x1="0%" y1="5%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#22c55e00" />
|
||||
<stop offset="100%" stop-color="#22c55e90" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M150 100 h500" opacity=".3" stroke="#3b82f6" stroke-width="4" stroke-dasharray="10, 15" stroke-linecap="round"/>
|
||||
<g transform="translate(150 100)">
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
|
||||
<rect x="-30" y="-19" width="60" height="38" rx="2" fill="#334155"/>
|
||||
<rect x="-28" y="-17" width="56" height="10" rx="2" fill="#555"/>
|
||||
<rect x="-28" y="-5" width="56" height="10" rx="2" fill="#555"/>
|
||||
<rect x="-28" y="7" width="56" height="10" rx="2" fill="#555"/>
|
||||
<text x="-26" y="-11" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">Test WKVS</text>
|
||||
|
||||
<text x="-26" y="-1" fill="#94a3b8" font-size="2.5" font-family="monospace" letter-spacing="0">WKVS,</text>
|
||||
<text x="-26" y="2.5" fill="#94a3b8" font-size="2" font-family="monospace" letter-spacing="0">EP</text>
|
||||
<rect x="0" y="-5" width="28" rx="2" height="10" fill="url(#myLinearGradient)" />
|
||||
|
||||
<text x="10" y="1" fill="#22c55e" font-size="4" font-family="monospace" letter-spacing="0.5">START</text>
|
||||
|
||||
<text x="-26" y="13" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">D 1.40</text>
|
||||
<text x="15" y="13" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">9.40</text>
|
||||
|
||||
|
||||
|
||||
<text y="70" text-anchor="middle" fill="#94a3b8" font-size="12" font-family="monospace" letter-spacing="1.5">Anzeige</text>
|
||||
</g>
|
||||
<g transform="translate(650 100)">
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
<rect x="-18" y="-15" width="36" height="10" rx="2" fill="#334155"/>
|
||||
<rect x="-18" y="2" width="36" height="10" rx="2" fill="#334155"/>
|
||||
<circle cx="10" cy="-10" r="2" fill="#10b981"/>
|
||||
<circle cx="10" cy="7" r="2" fill="#10b981"/>
|
||||
<text y="70" text-anchor="middle" fill="#94a3b8" font-size="12" font-family="monospace" letter-spacing="1.5">WKVS Server</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#a)" transform="translate(400 100)">
|
||||
<path d="m-15-60 15 30 m30-60 -30 60" stroke="#0e0" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
||||
<div class="divErrorTextWs">
|
||||
<p class="errortext">Keine WebSocket Verbindung</p>
|
||||
<p class="errortextSmall">Versuche Verbindung... (Versuch <span id="counterTries"></span> / <span id="counterMaxTries"></span>)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noWsConnection">
|
||||
<div class="rotator">
|
||||
<p>Keine WebSocket Verbindung, Syncronisation via FETCH</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logobg">
|
||||
<img id="jsImgLogo" class="logoimg <?= (!$displayCTextDisplayLogo) ? 'cTextHidden' : '' ?>" src="/intern/img/logo-normal.png">
|
||||
<p class="logotext <?= (!$displayCTextDisplayWKName) ? 'cTextHidden' : '' ?>"><?= $wkName ?></p>
|
||||
<p class="logoctext"></p>
|
||||
</div>
|
||||
|
||||
<div class="pagediv">
|
||||
<div class="display-row row1">
|
||||
<p class="row1text"></p>
|
||||
</div>
|
||||
<div class="display-row row2">
|
||||
<div class="row2text">
|
||||
<p class="row2_1text sds"></p>
|
||||
<p class="row2_2text"></p>
|
||||
</div>
|
||||
<div class="start_div">
|
||||
<p class="start_text"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="display-row row3">
|
||||
<p class="row3_1text"></p>
|
||||
<p class="row3_2text"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
const userAccess = "<?php echo $lastSegment; ?>_display";
|
||||
const jsonUrl = "<?php echo $jsonUrl; ?>";
|
||||
const jsonUrlconfig = "<?php echo $jsonUrlconfig; ?>";
|
||||
|
||||
// --- State Management ---
|
||||
let ws;
|
||||
let filedConectCount = 1;
|
||||
const fallbackConectCount = 5;
|
||||
const RETRY_DELAY = 3000;
|
||||
const FALLBACK_POLL_INTERVAL = 3000; // Fetch every 3 seconds if WS is dead
|
||||
const LONG_TERM_RETRY_DELAY_MULTIPLIER = 5;
|
||||
|
||||
let morphOnConnect = false;
|
||||
let morphOk = true;
|
||||
|
||||
const WSaccesstype = "<?= $WSaccesstype ?>";
|
||||
const WSaccess = "<?= $WSaccess ?>";
|
||||
const csrf_token = "<?= $csrf_token ?>";
|
||||
const WSaccessPermission = "<?= $WSaccessPermission ?>";
|
||||
let firstConnect = true;
|
||||
let longTermConnection = false;
|
||||
const urlAjaxNewWSToken = '/intern/scripts/ajax-create-ws-token.php';
|
||||
|
||||
let fallbackTimer = null;
|
||||
|
||||
// Hold our current data in memory
|
||||
let displayConfig = { type: 'logo', ctext: '' };
|
||||
let currentScore = {};
|
||||
let lastUniqueId = null;
|
||||
|
||||
const logobg = document.querySelector('.logobg');
|
||||
const ctext = document.querySelector('.logoctext');
|
||||
|
||||
// UI Elements
|
||||
let counterTriesEl = document.getElementById('counterTries');
|
||||
let counterMaxTriesEl = document.getElementById('counterMaxTries');
|
||||
|
||||
if(counterTriesEl) counterTriesEl.innerHTML = filedConectCount;
|
||||
if(counterMaxTriesEl) counterMaxTriesEl.innerHTML = fallbackConectCount;
|
||||
|
||||
// --- Fullscreen Listener ---
|
||||
const fs = document.documentElement;
|
||||
document.body.addEventListener('click', () => {
|
||||
if (fs.requestFullscreen) fs.requestFullscreen();
|
||||
else if (fs.webkitRequestFullscreen) fs.webkitRequestFullscreen();
|
||||
else if (fs.mozRequestFullScreen) fs.mozRequestFullScreen();
|
||||
else if (fs.msRequestFullscreen) fs.msRequestFullscreen();
|
||||
});
|
||||
|
||||
async function fetchNewWSToken(accesstype, access, accessPermission) {
|
||||
try {
|
||||
const response = await fetch(urlAjaxNewWSToken, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
csrf_token,
|
||||
accesstype,
|
||||
access,
|
||||
accessPermission
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const data = await response.json();
|
||||
return data.success ? data.token : null;
|
||||
} catch (error) {
|
||||
console.error("Token fetch failed:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- WebSocket Logic ---
|
||||
async function startWebSocket() {
|
||||
console.log("Attempting WebSocket connection...");
|
||||
|
||||
let token;
|
||||
if (firstConnect) {
|
||||
token = '<?= generateWSReadToken($WSaccesstype, $WSaccess) ?>';
|
||||
} else {
|
||||
token = await fetchNewWSToken(WSaccesstype, WSaccess, WSaccessPermission);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.error("No valid token available. Retrying...");
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ws = new WebSocket(`wss://${window.location.hostname}/ws/?access=token&token=${token}`);
|
||||
} catch (err) {
|
||||
console.error("Malformed WebSocket URL", err);
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket connected!");
|
||||
|
||||
if (morphOnConnect) {
|
||||
morph();
|
||||
} else {
|
||||
document.querySelector('.errorws').style.display = 'none';
|
||||
document.querySelector('.noWsConnection').style.display = 'none';
|
||||
document.querySelector('.pagediv').classList.remove("manuel");
|
||||
}
|
||||
|
||||
filedConectCount = 1;
|
||||
if(counterTriesEl) counterTriesEl.innerHTML = filedConectCount;
|
||||
|
||||
firstConnect = false;
|
||||
longTermConnection = false;
|
||||
morphOnConnect = false;
|
||||
morphOk = true;
|
||||
|
||||
// Stop fallback polling since WS is alive
|
||||
stopFallbackPolling();
|
||||
|
||||
// Do ONE initial fetch to sync current state
|
||||
fetchFullState();
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
if (!longTermConnection) {
|
||||
document.querySelector('.errorws').style.display = 'flex';
|
||||
document.querySelector('.errorws').style.opacity = '1';
|
||||
}
|
||||
morphOk = false;
|
||||
restore();
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (!longTermConnection) {
|
||||
document.querySelector('.errorws').style.display = 'flex';
|
||||
document.querySelector('.errorws').style.opacity = '1';
|
||||
}
|
||||
morphOnConnect = true;
|
||||
morphOk = false;
|
||||
restore();
|
||||
scheduleRetry();
|
||||
};
|
||||
|
||||
ws.addEventListener("message", msg => {
|
||||
let msgJSON;
|
||||
try {
|
||||
msgJSON = JSON.parse(msg.data);
|
||||
} catch (error) {
|
||||
return; // Ignore malformed messages
|
||||
}
|
||||
|
||||
// Route the incoming push data
|
||||
switch (msgJSON.type) {
|
||||
case "EINSTELLUNGEN_DISPLAY_UPDATE":
|
||||
updateSettings(msgJSON.payload.key, msgJSON.payload.value);
|
||||
break;
|
||||
case "UPDATE_DISPLAYCONTROL":
|
||||
// Expecting payload: { type: 'logo'|'ctext'|'scoring', ctext: '...' }
|
||||
displayConfig = msgJSON.payload;
|
||||
renderDOM();
|
||||
break;
|
||||
case "UPDATE_SCORE":
|
||||
// Expecting payload: the exact same structure as your jsonUrl outputs
|
||||
currentScore = msgJSON.payload;
|
||||
renderDOM();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleRetry() {
|
||||
|
||||
if (longTermConnection) {
|
||||
setTimeout(startWebSocket, RETRY_DELAY * LONG_TERM_RETRY_DELAY_MULTIPLIER);
|
||||
} else {
|
||||
console.log(`Retrying in ${RETRY_DELAY}ms...`);
|
||||
|
||||
if (filedConectCount >= fallbackConectCount) {
|
||||
// MAX RETRIES REACHED -> Enter Fallback Mode
|
||||
longTermConnection = true;
|
||||
document.querySelector('.errorws').style.display = 'none';
|
||||
document.querySelector('.noWsConnection').style.display = 'flex';
|
||||
document.querySelector('.pagediv').classList.add("manuel");
|
||||
startFallbackPolling();
|
||||
} else {
|
||||
|
||||
if(counterTriesEl) counterTriesEl.innerHTML = filedConectCount;
|
||||
setTimeout(startWebSocket, RETRY_DELAY);
|
||||
filedConectCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fallback Polling Logic ---
|
||||
function startFallbackPolling() {
|
||||
if (fallbackTimer === null) {
|
||||
console.warn("Starting JSON fallback polling...");
|
||||
fetchFullState(); // Fetch immediately once
|
||||
fallbackTimer = setInterval(fetchFullState, FALLBACK_POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
function stopFallbackPolling() {
|
||||
if (fallbackTimer !== null) {
|
||||
clearInterval(fallbackTimer);
|
||||
fallbackTimer = null;
|
||||
console.log("Stopped JSON fallback polling.");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Data Fetching (Initial Sync & Fallback) ---
|
||||
async function fetchFullState() {
|
||||
try {
|
||||
// Fetch both config and score simultaneously
|
||||
const [resConfig, resScore] = await Promise.all([
|
||||
fetch(jsonUrlconfig + '?t=' + Date.now(), { cache: "no-store" }),
|
||||
fetch(jsonUrl + '?t=' + Date.now(), { cache: "no-store" })
|
||||
]);
|
||||
|
||||
if (resConfig.ok) displayConfig = await resConfig.json();
|
||||
if (resScore.ok) currentScore = await resScore.json();
|
||||
|
||||
renderDOM();
|
||||
} catch (err) {
|
||||
console.error("Error fetching JSON:", err);
|
||||
const container = document.getElementById('score');
|
||||
if(container) {
|
||||
container.innerHTML = "";
|
||||
container.style.backgroundColor = "black";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- The Master Renderer ---
|
||||
function renderDOM() {
|
||||
|
||||
if (displayConfig.type === 'logo' || displayConfig.type === 'ctext') {
|
||||
// LOGO OR CUSTOM TEXT MODE
|
||||
if (logobg) {
|
||||
logobg.style.opacity = "1";
|
||||
|
||||
if (displayConfig.type === 'logo') logobg.classList.remove('ctext');
|
||||
if (displayConfig.type === 'ctext') logobg.classList.add('ctext');
|
||||
}
|
||||
if (ctext) {
|
||||
ctext.innerText = (displayConfig.type === 'ctext') ? (displayConfig.ctext || '') : '';
|
||||
resizeCText();
|
||||
}
|
||||
|
||||
} else if (displayConfig.type === 'scoring') {
|
||||
// SCORING MODE
|
||||
if (logobg) logobg.style.opacity = "0";
|
||||
|
||||
if (currentScore.uniqueid !== lastUniqueId) {
|
||||
lastUniqueId = currentScore.uniqueid;
|
||||
// Reset any animation/repeat logic here if needed
|
||||
}
|
||||
|
||||
const safeText = (selector, text) => {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) el.innerText = text !== undefined ? text : '';
|
||||
};
|
||||
|
||||
safeText('.row1text', `${currentScore.vorname || ''} ${currentScore.name || ''}`);
|
||||
safeText('.row2_1text', currentScore.verein ? `${currentScore.verein}, ` : '');
|
||||
safeText('.row2_2text', currentScore.programm || '');
|
||||
|
||||
const starttext = document.querySelector('.start_text');
|
||||
const row2El = document.querySelector('.row2');
|
||||
if (starttext && row2El) {
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
const dangerColor = rootStyles.getPropertyValue('--display-danger').trim();
|
||||
const successColor = rootStyles.getPropertyValue('--display-success').trim();
|
||||
|
||||
if (currentScore.start === true) {
|
||||
row2El.style.setProperty('--display-colorStartDiv', successColor);
|
||||
starttext.innerHTML = 'Start';
|
||||
} else {
|
||||
row2El.style.setProperty('--display-colorStartDiv', dangerColor);
|
||||
starttext.innerHTML = 'Stop';
|
||||
}
|
||||
}
|
||||
|
||||
safeText('.row3_1text', currentScore.noteLinks);
|
||||
safeText('.row3_2text', currentScore.noteRechts);
|
||||
}
|
||||
|
||||
fitTextAll();
|
||||
}
|
||||
|
||||
// --- Settings & UI Handlers ---
|
||||
function updateSettings(type, value) {
|
||||
|
||||
const sanitizeHex = (val) => val.replace(/[^0-9a-fA-F#]/g, '');
|
||||
|
||||
switch (type) {
|
||||
case 'wkName':
|
||||
const logotext = document.querySelector('.logotext');
|
||||
if (logotext) logotext.innerHTML = value;
|
||||
break;
|
||||
case 'displayColourLogo':
|
||||
document.documentElement.style.setProperty('--display-panelBgLogo', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayTextColourLogo':
|
||||
document.documentElement.style.setProperty('--display-text-color', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayCTextLogo':
|
||||
document.querySelector('.logoimg').classList.toggle('cTextHidden');
|
||||
break;
|
||||
case 'displayCTextWKName':
|
||||
document.querySelector('.logotext').classList.toggle('cTextHidden');
|
||||
break;
|
||||
case 'displayColorScoringBg':
|
||||
document.documentElement.style.setProperty('--display-bg', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringBgSoft':
|
||||
document.documentElement.style.setProperty('--display-bg-soft', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringShadowColor':
|
||||
document.documentElement.style.setProperty('--display-shadow', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringBorderColor':
|
||||
document.documentElement.style.setProperty('--display-border', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanel':
|
||||
document.documentElement.style.setProperty('--display-panel', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelSoft':
|
||||
document.documentElement.style.setProperty('--display-panel-soft', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelText':
|
||||
document.documentElement.style.setProperty('--display-text-main', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelTextSoft':
|
||||
document.documentElement.style.setProperty('--display-text-muted', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelTextNoteL': // Matching your PHP typo
|
||||
document.documentElement.style.setProperty('--display-color-note-l', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelTextNoteR': // Matching your PHP typo
|
||||
document.documentElement.style.setProperty('--display-color-note-r', sanitizeHex(value));
|
||||
break;
|
||||
case 'logo-normal':
|
||||
const jsImgLogo = document.getElementById('jsImgLogo');
|
||||
if(jsImgLogo) jsImgLogo.src = '/intern/img/logo-normal.png?' + Date.now();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Text Resizing Engine ---
|
||||
function isOverflown(parent, elem, heightscale, widthscale, paddingtext) {
|
||||
return (
|
||||
(elem.scrollWidth + paddingtext) > (parent.clientWidth / widthscale) ||
|
||||
(elem.scrollHeight + paddingtext) > (parent.clientHeight / heightscale)
|
||||
);
|
||||
}
|
||||
|
||||
function fitTextElement(elem, { minSize = 10, maxSize = 1000, step = 1, unit = 'px' } = {}) {
|
||||
if (!elem) return;
|
||||
const parent = elem.parentElement;
|
||||
if (!parent) return;
|
||||
|
||||
// FIXED: Declare variables properly so they don't leak into the global scope
|
||||
let heightscale = 1;
|
||||
let widthscale = 1;
|
||||
let paddingtext = 60;
|
||||
|
||||
if (parent.classList.contains('row2text')) {
|
||||
heightscale = 2;
|
||||
paddingtext = 0;
|
||||
}
|
||||
if (parent.classList.contains('row3')) {
|
||||
widthscale = 2;
|
||||
}
|
||||
|
||||
let size = minSize;
|
||||
elem.style.whiteSpace = 'nowrap';
|
||||
elem.style.fontSize = size + unit;
|
||||
|
||||
while (size < maxSize && !isOverflown(parent, elem, heightscale, widthscale, paddingtext)) {
|
||||
size += step;
|
||||
elem.style.fontSize = size + unit;
|
||||
}
|
||||
|
||||
elem.style.fontSize = (size - step) + unit;
|
||||
}
|
||||
|
||||
function resizeCText() {
|
||||
ctext.style.fontSize = '';
|
||||
if (isOverflown(logobg, ctext, 1, 1, 0)) fitTextElement(ctext);
|
||||
}
|
||||
|
||||
function fitTextAll() {
|
||||
const paragraphs = document.querySelectorAll('.pagediv p');
|
||||
paragraphs.forEach(p => fitTextElement(p));
|
||||
resizeCText();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', fitTextAll);
|
||||
|
||||
// --- Initialize ---
|
||||
startWebSocket();
|
||||
|
||||
async function morph() {
|
||||
gsap.to("#cross", {
|
||||
duration: 1,
|
||||
attr: {
|
||||
d: "M-15,-60 L0,-30 M30,-90 L0,-30"
|
||||
},
|
||||
stroke: "#0e0",
|
||||
ease: "power2.inOut"
|
||||
});
|
||||
|
||||
gsap.to(".opacity0gsap", {
|
||||
duration: 1,
|
||||
opacity: "0",
|
||||
ease: "back.out(1.7)"
|
||||
});
|
||||
|
||||
const pathStart = "M150 100 h200 m100 0 h200";
|
||||
|
||||
const pathEnd = "M150 100 h250 m0 0 h250";
|
||||
|
||||
gsap.to("#errorLine", {
|
||||
duration: 1,
|
||||
attr: { d: pathEnd },
|
||||
ease: "power2.inOut"
|
||||
});
|
||||
|
||||
gsap.to(".errortext", {
|
||||
duration: 1,
|
||||
textContent: "Verbindung Wiederhergestellt",
|
||||
ease: "back.out(1.7)"
|
||||
});
|
||||
|
||||
gsap.to(".errortextSmall", {
|
||||
duration: 1,
|
||||
textContent: "Stelle auf Wettkampfbetrieb um...",
|
||||
ease: "back.out(1.7)"
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (morphOk) {
|
||||
gsap.to('.errorws', {
|
||||
opacity: 0,
|
||||
duration: 0.5,
|
||||
overwrite: "auto"
|
||||
});
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
setTimeout(() => {
|
||||
if (morphOk) {
|
||||
document.querySelector('.errorws').style.display = 'none';
|
||||
}
|
||||
}, 3800);
|
||||
}
|
||||
|
||||
function restore() {
|
||||
// 1. Reset the Cross/Check path and color
|
||||
gsap.set("#cross", {
|
||||
attr: { d: "M-30,-30 L30,30 M30,-30 L-30,30" },
|
||||
stroke: "#f43f5e"
|
||||
});
|
||||
|
||||
// 2. Reset opacity for elements with that class
|
||||
gsap.set(".opacity0gsap", {
|
||||
opacity: ".4"
|
||||
});
|
||||
|
||||
// 3. Reset the Error Line path
|
||||
gsap.set("#errorLine", {
|
||||
attr: { d: "M150 100 h200 m100 0 h200" }
|
||||
});
|
||||
|
||||
gsap.to(".errortext", {
|
||||
duration: 1,
|
||||
textContent: "Keine WebSocket Verbindung",
|
||||
ease: "back.out(1.7)"
|
||||
});
|
||||
|
||||
document.querySelector(".errortextSmall").innerHTML = 'Versuche Verbindung... (Versuch <span id="counterTries"></span> / <span id="counterMaxTries"></span>)';
|
||||
|
||||
counterTriesEl = document.getElementById('counterTries');
|
||||
counterMaxTriesEl = document.getElementById('counterMaxTries');
|
||||
|
||||
if(counterTriesEl) counterTriesEl.innerHTML = filedConectCount;
|
||||
if(counterMaxTriesEl) counterMaxTriesEl.innerHTML = fallbackConectCount;
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
$request = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
|
||||
$segments = $request === '' ? [] : explode('/', $request);
|
||||
|
||||
$segments = array_map('urldecode', $segments);
|
||||
|
||||
$type = $segments[1] ?? null;
|
||||
$slug = $segments[2] ?? null;
|
||||
|
||||
$allowed_types = ["display", "audioplayer"];
|
||||
|
||||
if (($type !== null && !in_array($type, $allowed_types)) || count($segments) > 3) {
|
||||
http_response_code(400);
|
||||
include $baseDir . "/error-pages/400.html";
|
||||
exit;
|
||||
}
|
||||
|
||||
define('APP_ROUTER', true);
|
||||
|
||||
if ($type === "display") {
|
||||
require __DIR__ . "/display.php";
|
||||
} elseif ($type === "audioplayer") {
|
||||
require __DIR__ . "/audio.php";
|
||||
}
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,91 @@
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,112 @@
|
||||
Google Sans Flex Variable Font
|
||||
==============================
|
||||
|
||||
This download contains Google Sans Flex as both a variable font and static fonts.
|
||||
|
||||
Google Sans Flex is a variable font with these axes:
|
||||
GRAD
|
||||
ROND
|
||||
opsz
|
||||
slnt
|
||||
wdth
|
||||
wght
|
||||
|
||||
This means all the styles are contained in a single file:
|
||||
GoogleSansFlex-VariableFont_GRAD,ROND,opsz,slnt,wdth,wght.ttf
|
||||
|
||||
If your app fully supports variable fonts, you can now pick intermediate styles
|
||||
that aren’t available as static fonts. Not all apps support variable fonts, and
|
||||
in those cases you can use the static font files for Google Sans Flex:
|
||||
static/GoogleSansFlex_9pt-Thin.ttf
|
||||
static/GoogleSansFlex_9pt-ExtraLight.ttf
|
||||
static/GoogleSansFlex_9pt-Light.ttf
|
||||
static/GoogleSansFlex_9pt-Regular.ttf
|
||||
static/GoogleSansFlex_9pt-Medium.ttf
|
||||
static/GoogleSansFlex_9pt-SemiBold.ttf
|
||||
static/GoogleSansFlex_9pt-Bold.ttf
|
||||
static/GoogleSansFlex_9pt-ExtraBold.ttf
|
||||
static/GoogleSansFlex_9pt-Black.ttf
|
||||
static/GoogleSansFlex_24pt-Thin.ttf
|
||||
static/GoogleSansFlex_24pt-ExtraLight.ttf
|
||||
static/GoogleSansFlex_24pt-Light.ttf
|
||||
static/GoogleSansFlex_24pt-Regular.ttf
|
||||
static/GoogleSansFlex_24pt-Medium.ttf
|
||||
static/GoogleSansFlex_24pt-SemiBold.ttf
|
||||
static/GoogleSansFlex_24pt-Bold.ttf
|
||||
static/GoogleSansFlex_24pt-ExtraBold.ttf
|
||||
static/GoogleSansFlex_24pt-Black.ttf
|
||||
static/GoogleSansFlex_36pt-Thin.ttf
|
||||
static/GoogleSansFlex_36pt-ExtraLight.ttf
|
||||
static/GoogleSansFlex_36pt-Light.ttf
|
||||
static/GoogleSansFlex_36pt-Regular.ttf
|
||||
static/GoogleSansFlex_36pt-Medium.ttf
|
||||
static/GoogleSansFlex_36pt-SemiBold.ttf
|
||||
static/GoogleSansFlex_36pt-Bold.ttf
|
||||
static/GoogleSansFlex_36pt-ExtraBold.ttf
|
||||
static/GoogleSansFlex_36pt-Black.ttf
|
||||
static/GoogleSansFlex_72pt-Thin.ttf
|
||||
static/GoogleSansFlex_72pt-ExtraLight.ttf
|
||||
static/GoogleSansFlex_72pt-Light.ttf
|
||||
static/GoogleSansFlex_72pt-Regular.ttf
|
||||
static/GoogleSansFlex_72pt-Medium.ttf
|
||||
static/GoogleSansFlex_72pt-SemiBold.ttf
|
||||
static/GoogleSansFlex_72pt-Bold.ttf
|
||||
static/GoogleSansFlex_72pt-ExtraBold.ttf
|
||||
static/GoogleSansFlex_72pt-Black.ttf
|
||||
static/GoogleSansFlex_120pt-Thin.ttf
|
||||
static/GoogleSansFlex_120pt-ExtraLight.ttf
|
||||
static/GoogleSansFlex_120pt-Light.ttf
|
||||
static/GoogleSansFlex_120pt-Regular.ttf
|
||||
static/GoogleSansFlex_120pt-Medium.ttf
|
||||
static/GoogleSansFlex_120pt-SemiBold.ttf
|
||||
static/GoogleSansFlex_120pt-Bold.ttf
|
||||
static/GoogleSansFlex_120pt-ExtraBold.ttf
|
||||
static/GoogleSansFlex_120pt-Black.ttf
|
||||
|
||||
Get started
|
||||
-----------
|
||||
|
||||
1. Install the font files you want to use
|
||||
|
||||
2. Use your app's font picker to view the font family and all the
|
||||
available styles
|
||||
|
||||
Learn more about variable fonts
|
||||
-------------------------------
|
||||
|
||||
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
|
||||
https://variablefonts.typenetwork.com
|
||||
https://medium.com/variable-fonts
|
||||
|
||||
In desktop apps
|
||||
|
||||
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
|
||||
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
|
||||
|
||||
Online
|
||||
|
||||
https://developers.google.com/fonts/docs/getting_started
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
||||
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
|
||||
|
||||
Installing fonts
|
||||
|
||||
MacOS: https://support.apple.com/en-us/HT201749
|
||||
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
|
||||
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
|
||||
|
||||
Android Apps
|
||||
|
||||
https://developers.google.com/fonts/docs/android
|
||||
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
|
||||
|
||||
License
|
||||
-------
|
||||
Please read the full license text (OFL.txt) to understand the permissions,
|
||||
restrictions and requirements for usage, redistribution, and modification.
|
||||
|
||||
You can use them in your products & projects – print or digital,
|
||||
commercial or otherwise.
|
||||
|
||||
This isn't legal advice, please consider consulting a lawyer and see the full
|
||||
license for all details.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
@font-face {
|
||||
font-family: "Google Sans Flex";
|
||||
src: url(/files/fonts/Google_Sans_Flex/GoogleSansFlex-VariableFont_GRAD,ROND,opsz,slnt,wdth,wght.ttf);
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: "Google Sans Flex", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-style: normal;
|
||||
font-variation-settings:
|
||||
"slnt" 0,
|
||||
"wdth" 100,
|
||||
"GRAD" 0,
|
||||
"ROND" 0;
|
||||
}
|
||||
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
RewriteEngine On
|
||||
|
||||
# Do not interfere with real files or directories
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
|
||||
# If a matching .php file exists, serve it
|
||||
RewriteCond %{REQUEST_FILENAME}.php -f
|
||||
RewriteRule ^(.+)$ $1.php [L]
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,206 @@
|
||||
.msgDiv {
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
flex-direction: column-reverse;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.msgDiv, .msgDiv > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.msgBox,
|
||||
.confirmBox {
|
||||
transform: translateX(calc(100% + 40px)) scaleY(0);
|
||||
height: 0;
|
||||
margin-top: 0;
|
||||
padding: 10px 15px;
|
||||
color: #fff;
|
||||
background-color: #0b0b0b;
|
||||
border-left: 4px solid;
|
||||
border-radius: 2px;
|
||||
transition: all 1s ease;
|
||||
}
|
||||
|
||||
.msgBox.show,
|
||||
.confirmBox.show {
|
||||
transform: translateX(0) scaleY(1);
|
||||
height: 40px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.msgBox.show {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.confirmBox.show {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.buttonConfirmDiv {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.confirmBox button {
|
||||
padding: 8px 24px;
|
||||
transition: background-color 0.3s ease, opacity 1s ease, height 1s ease;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.confirmBox.show button {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.confirmYesButton {
|
||||
background-color: rgba(0, 255, 0, 0.75);
|
||||
}
|
||||
|
||||
.confirmNoButton {
|
||||
background-color: rgba(255, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.confirmYesButton:hover {
|
||||
background-color: rgba(0, 255, 0, 1);
|
||||
}
|
||||
|
||||
.confirmNoButton:hover {
|
||||
background-color: rgba(255, 0, 0, 1);
|
||||
}
|
||||
|
||||
.confirmImportantHeading {
|
||||
font-size: 22px;
|
||||
margin: 0;
|
||||
letter-spacing: 1px;
|
||||
color: #36454F;
|
||||
}
|
||||
|
||||
.confirmImportantText {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
color: #36454F;
|
||||
}
|
||||
|
||||
.confirmImportantLabel {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
color: #36454F;
|
||||
}
|
||||
|
||||
.confirmImportantInput {
|
||||
border: 1px solid #36454F;
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
color: #36454F;
|
||||
}
|
||||
|
||||
.confirmImportantInput.notOk {
|
||||
color: #692727;
|
||||
}
|
||||
|
||||
.confirmImportantInput.ok {
|
||||
color: #2b6927;
|
||||
}
|
||||
|
||||
.confirmImportantInput:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.confirmImportantInput.notOk:focus {
|
||||
color: #8b0000;
|
||||
border-color: #692727;
|
||||
}
|
||||
|
||||
.confirmImportantInput.ok:focus {
|
||||
color: #008b00;
|
||||
border-color: #2b6927;
|
||||
}
|
||||
|
||||
|
||||
.confirmImportantInputDiv {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.confirmImportantConfrimBtn, .confirmImportantCancelBtn {
|
||||
background: #25789e;
|
||||
border: 1px solid #fff;
|
||||
color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
transition: transform 0.3s ease filter 0.5s ease;
|
||||
letter-spacing: 1px;
|
||||
display: flex;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.confirmImportantConfrimBtn {
|
||||
background: #25789e;
|
||||
}
|
||||
|
||||
.confirmImportantCancelBtn {
|
||||
background: #9e253d;
|
||||
}
|
||||
|
||||
.confirmImportantInputDiv button:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.confirmImportantInputDiv button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.notValidBtn {
|
||||
user-select: none;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.confirmImportantBtnDiv {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.confirmImportantTextDiv {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.confirmImportantBox {
|
||||
display: grid;
|
||||
padding: 20px;
|
||||
grid-template-columns: 1fr;
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
width: min(500px, 90vw);
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.confirmImportantBg {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
z-index: 10000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(2px);
|
||||
background-color: #36454f2f;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
.customSelect {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.customSelect>* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.selectTrigger .selectArrow {
|
||||
transition: rotate 0.6s ease;
|
||||
}
|
||||
|
||||
table input {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.selectTrigger,
|
||||
.selectTriggerBulk {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selectOptions,
|
||||
.selectOptionsBulk {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 4px;
|
||||
list-style: none;
|
||||
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
|
||||
background: #fff;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
pointer-events: none;
|
||||
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.customSelect.open .selectOptions,
|
||||
.customSelect.open .selectOptionsBulk {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.customSelect.open .selectArrow {
|
||||
rotate: 180deg
|
||||
}
|
||||
|
||||
.selectOptions li,
|
||||
.selectOptionsBulk li {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2px;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.selectOptions li:last-child,
|
||||
.selectOptionsBulk li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.selectOptions li:hover,
|
||||
.selectOptionsBulk li:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.selectOptions li.selected,
|
||||
.selectOptionsBulk li.selected {
|
||||
background-color: #ccc;
|
||||
color: #000;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.selectLabel:not(.sidebar-links .selectLabel) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
:root {
|
||||
--main: #6e285f;
|
||||
--bg-top-raw: 110 40 95;
|
||||
--main-box-shadow: rgb(from var(--main) r g b / 0.05);
|
||||
--accent: #737444;
|
||||
--paddingSite: 40px;
|
||||
--card-radius: 20px;
|
||||
--card-bg: #ffffff;
|
||||
--bg: #FDFDFD;
|
||||
--card-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
|
||||
--accent-soft: #f0f5ff;
|
||||
--border-subtle: #d4d7e1;
|
||||
--text-main: #191919;
|
||||
--text-muted: #5e5e5e;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
section {
|
||||
margin: 0;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--bg);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.bgSection {
|
||||
padding: var(--paddingSite);
|
||||
}
|
||||
|
||||
.headerDivTrainer {
|
||||
padding: var(--paddingSite);
|
||||
background-color: var(--main);
|
||||
color: var(--bg);
|
||||
margin-bottom: var(--paddingSite);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.headingPanel {
|
||||
margin: 0;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.controls-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
margin-bottom: var(--paddingSite);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.controls-wrapper>div,
|
||||
.controls-wrapper>form {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* From Trainer Dashboard - newBtn style */
|
||||
button.change-type,
|
||||
button[type="submit"] {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
transition: all 0.2s;
|
||||
padding: 10px 24px;
|
||||
border-radius: 100px;
|
||||
background: #cfef00;
|
||||
border: 1px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
button.change-type:hover,
|
||||
button[type="submit"]:hover {
|
||||
background: #c5e300;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button.change-type:active,
|
||||
button[type="submit"]:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.change-type-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.change-type-form textarea {
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: #fff;
|
||||
font-size: 15px;
|
||||
width: 250px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.change-type-form textarea:focus {
|
||||
border-color: var(--main);
|
||||
}
|
||||
|
||||
.iframe-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.iframeWithTitle {
|
||||
background: #000;
|
||||
border-radius: 20px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
:root {
|
||||
--paddingSite: 20px;
|
||||
}
|
||||
|
||||
.iframeWithTitle {
|
||||
max-width: calc(100vw - 2 * var(--paddingSite));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.iframeWithTitle h1 {
|
||||
color: #fff;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.divSucsess {
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
transform: translateX(400px);
|
||||
background-color: #ffffff;
|
||||
z-index: 10000;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
border-left: 5px solid #4ade80;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.divSucsess.show {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.textSucsess {
|
||||
color: #137333;
|
||||
margin: 15px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+13
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,147 @@
|
||||
body{
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.page-secure-login{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bg-secure-login-form {
|
||||
gap: 20px;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.bg-picture-secure-login{
|
||||
width: calc(100vw - 550px);
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.bg-picture-secure-login img{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 0 80px 0 0;
|
||||
display: block;
|
||||
}
|
||||
.bg-secure-login{
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
max-width: 550px;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30px;
|
||||
}
|
||||
.bg-secure-login-form > h1{
|
||||
color: #36454F !important;
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.bg-secure-login-form input[type=password], .bg-secure-login-form input[type=text]{
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
font-size: 16px;
|
||||
border-bottom: 1px solid #000 !important;
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
#access_username {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.bg-secure-login-form input[type=password]:focus, .bg-secure-login-form input[type=text]:focus{
|
||||
outline: none;
|
||||
border-bottom: 1px solid #000 !important;
|
||||
}
|
||||
|
||||
.bg-secure-login-form input[type=password]::placeholder, .bg-secure-login-form input[type=text]::placeholder {
|
||||
color: #ccc !important;
|
||||
}
|
||||
|
||||
.bg-secure-login-form input[type=submit]{
|
||||
background-color: #fff !important;
|
||||
padding: 10px 20px !important;
|
||||
margin-top: 25px !important;
|
||||
border: 1px solid #000 !important;
|
||||
color: #36454F !important;
|
||||
transition: all 0.3s ease-out !important;
|
||||
border-radius: 0px !important;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.bg-secure-login-form input[type=submit]:hover{
|
||||
background-color: #36454F !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.bg-secure-login-form > p{
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.seclog_home_link{
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.seclog_home_link > img{
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
#div_showpw, #access_username {
|
||||
margin-top: 10px;
|
||||
}
|
||||
#togglePassword {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
#togglePassword:hover {
|
||||
transform: translateY(-50%) scale(1.15);
|
||||
}
|
||||
|
||||
#div_showpw{
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
label {
|
||||
color: #36454F !important;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 1000px #ffffff inset !important;
|
||||
box-shadow: 0 0 0 1000px #ffffff inset !important;
|
||||
-webkit-text-fill-color: #000000 !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,120 @@
|
||||
body{
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-secure-login{
|
||||
display: flex;
|
||||
}
|
||||
.bg-picture-secure-login{
|
||||
width: calc(100vw - 450px);
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
.bg-picture-secure-login img{
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
object-fit: cover;
|
||||
}
|
||||
.bg-secure-login{
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
max-width: 450px;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
align-items: center;
|
||||
padding: 30px;
|
||||
}
|
||||
.bg-secure-login-form > h1{
|
||||
color: #000 !important;
|
||||
font-size: 32px;
|
||||
}
|
||||
.bg-secure-login-form input[type=password], .bg-secure-login-form input[type=text]{
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
font-size: 16px;
|
||||
border-bottom: 1px solid #000 !important;
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
.divShowPw:not(#lastDivShowPw) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.bg-secure-login-form input[type=password]:focus, .bg-secure-login-form input[type=text]:focus{
|
||||
outline: none;
|
||||
border-bottom: 1px solid #000 !important;
|
||||
}
|
||||
|
||||
.bg-secure-login-form input[type=password]::placeholder, .bg-secure-login-form input[type=text]::placeholder {
|
||||
color: #ccc !important;
|
||||
}
|
||||
|
||||
.bg-secure-login-form input[type=submit]{
|
||||
background-color: #fff !important;
|
||||
padding: 10px 20px !important;
|
||||
margin-top: 25px !important;
|
||||
border: 1px solid #000 !important;
|
||||
color: #000 !important;
|
||||
transition: all 0.3s ease-out !important;
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bg-secure-login-form input[type=submit]:hover{
|
||||
background-color: #000 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.bg-secure-login-form > p{
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.seclog_home_link{
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
}
|
||||
.divShowPw {
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
.togglePassword {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.togglePassword:hover {
|
||||
transform: translateY(-50%) scale(1.15);
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 1000px #ffffff inset !important;
|
||||
box-shadow: 0 0 0 1000px #ffffff inset !important;
|
||||
-webkit-text-fill-color: #000000 !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
:root {
|
||||
--paddingSite: 40px;
|
||||
--card-radius: 24px;
|
||||
--card-bg: #ffffff;
|
||||
--bg: #F4F3EF;
|
||||
--card-shadow: 0 12px 40px rgba(0, 0, 0, 0.04);
|
||||
--accent: #FF5A5F;
|
||||
/* Soft energetic pink/red from Dribbble */
|
||||
--accent-secondary: #FF8A65;
|
||||
--bg-top: rgb(54, 137, 13);
|
||||
--bg-top-raw: 54 137 13;
|
||||
--accent-soft: #f0f5ff;
|
||||
--border-subtle: #eaeaea;
|
||||
--text-main: #1b1b1b;
|
||||
--text-muted: #8A8A8A;
|
||||
--disabled-bg: #f3f4f6;
|
||||
--disabled-border: #cbd5f5;
|
||||
--ui-border: #dfe4ea;
|
||||
--main-button: #36454F;
|
||||
}
|
||||
|
||||
/* Base */
|
||||
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
section {
|
||||
margin: 0;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #0000003d;
|
||||
color: #ffffffff;
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: #a4bf4a99;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
input {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* General numeric inputs in scoring area */
|
||||
.bgSection input[type="number"] {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.nopadding {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
|
||||
.bgSection {
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
/*margin: var(--paddingSite);
|
||||
width: calc(100vw - 2 * var(--paddingSite));*/
|
||||
width: 100vw;
|
||||
box-sizing: border-box !important;
|
||||
background: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1081px) {
|
||||
.bgSection.open {
|
||||
width: calc(100vw - 380px);
|
||||
/* - 2 * var(--paddingSite) */
|
||||
}
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
|
||||
.headerDivTrainer {
|
||||
padding: var(--paddingSite);
|
||||
padding-bottom: 10px;
|
||||
background: transparent;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.dashboardGrid {
|
||||
padding: 0 var(--paddingSite) var(--paddingSite);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.twoColumDiv {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.twoColumDiv {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* headings */
|
||||
|
||||
.containerHeading {
|
||||
margin-top: 0;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
margin-bottom: 24px;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.subSettingsRowGroup .settingsRow span {
|
||||
color: #36454f;
|
||||
}
|
||||
|
||||
.subSettingsRowGroup {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
align-items: end;
|
||||
gap: 16px 22px;
|
||||
padding: 20px;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.customSelectSubGroupHeader {
|
||||
border-bottom: 1px solid #1c2d34;
|
||||
font-weight: 600;
|
||||
border-radius: 0 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.customSelectAddOption {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig > span {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
border: 1px solid #00000046;
|
||||
}
|
||||
|
||||
#bodyRowRankLiveConfig > td > span {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 15px;
|
||||
position: relative;
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #00000046;
|
||||
position: relative;
|
||||
min-height: 54.5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig .addColumnBtn {
|
||||
font-size: 24px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
line-height: 0;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
background-color: #5d9ab9;
|
||||
border: 1px solid #3a6b83;
|
||||
color: #fff;
|
||||
border-radius: 15px;
|
||||
align-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig .addColumnBtn.addColumnBtnAfter {
|
||||
right: 0;
|
||||
transform: translateX(50%) translateY(-50%);
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig .addColumnBtn.addColumnBtnBefore {
|
||||
left: 0;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig .addColumnBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig:not(.ui-sortable-helper) > span:hover .addColumnBtn.addColumnBtnAfter {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig:not(.ui-sortable-helper):first-child > span:hover .addColumnBtn.addColumnBtnBefore {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig:not(.ui-sortable-helper) > span:hover {
|
||||
border-right: 1px solid #3a6b83;
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig:not(.ui-sortable-helper):first-child > span:hover {
|
||||
border-left: 1px solid #3a6b83;
|
||||
}
|
||||
|
||||
|
||||
.deleteColumnBtn {
|
||||
color: #6a2a2a;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.customSelectAddOption {
|
||||
max-width: min(100vw, 600px);
|
||||
border: 1px solid #777;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.rankLiveFieldContainerEmty {
|
||||
display: block;
|
||||
min-height: 30px;
|
||||
width: 100%;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
#eeeeee50,
|
||||
#eeeeee50 5px,
|
||||
#dddddd50 5px,
|
||||
#dddddd50 10px
|
||||
);
|
||||
}
|
||||
|
||||
.singleValueSpan {
|
||||
position: relative;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #777;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.staticText {
|
||||
border-color: #00f;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.staticText .textSingleValueSpan {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.settingSpan {
|
||||
display: none;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
background-color: #09d1ce;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
transform: translateX(50%) translateY(-50%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig .settingSpan {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
.singleValueSpan .settingSpan {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.mainTable:not(.selectCondEl) .singleValueSpan:hover .settingSpan,
|
||||
.mainTable:not(.selectCondEl) .headerThRankLiveConfig:hover .settingSpan,
|
||||
.settingsDivSingleValueSpan:not(.hidden) + .settingSpan {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.settingSpan > svg {
|
||||
display: block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.settingsDivSingleValueSpan {
|
||||
background-color: #fff;
|
||||
padding: 15px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateY(100%);
|
||||
min-width: 100%;
|
||||
width: fit-content;
|
||||
gap: 12px;
|
||||
z-index: 10;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.settingsInputGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.singleValueSpan[data-bold="true"] .textSingleValueSpan, .singleValueSpan[data-bold="true"] .statischerTextInput {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.addFieldBtn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #36e049;
|
||||
display: flex;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-weight: 800;
|
||||
border-radius: 0 0 10px 0;
|
||||
filter: grayscale(1);
|
||||
transform: translateY(-37.5%) translateX(-37.5%) scale(0.25);
|
||||
transition: transform 0.3s ease, scale 0.3s ease;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.mainTable.selectCondEl .addFieldBtn {
|
||||
transform: scale(0) translateY(-50%) translateX(-50%);
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
.mainTable:not(.selectCondEl) .rankLiveFieldContainer:hover:not(:has(.singleValueSpan:hover)) .addFieldBtn {
|
||||
filter: grayscale(0);
|
||||
transform: scale(1) translateY(0) translateX(0);
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig > span .deleteColumnBtn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: #e03669;
|
||||
display: flex;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-weight: 800;
|
||||
border-radius: 0 0 0 10px;
|
||||
filter: grayscale(1);
|
||||
transform: translateY(-37.5%) translateX(37.5%) scale(0.25);
|
||||
transition: transform 0.3s ease, scale 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.headerThRankLiveConfig:not(.ui-sortable-helper) > span:hover .deleteColumnBtn {
|
||||
filter: grayscale(0);
|
||||
transform: scale(1) translateY(0) translateX(0);
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: max-content;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.checkbox-ui {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 42px;
|
||||
height: 24px;
|
||||
flex: 0 0 auto;
|
||||
background: var(--ui-border);
|
||||
border: 1px solid color-mix(in srgb, var(--ui-border) 80%, #000);
|
||||
border-radius: 999px;
|
||||
transition:
|
||||
background-color 180ms ease,
|
||||
border-color 180ms ease,
|
||||
box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.checkbox-ui::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 3px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 7px rgba(15, 23, 42, 0.22);
|
||||
transform: translateY(-50%);
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
.checkbox input:checked + .checkbox-ui {
|
||||
background: var(--bg-top);
|
||||
border-color: var(--bg-top);
|
||||
}
|
||||
|
||||
.checkbox input:checked + .checkbox-ui::after {
|
||||
transform: translate(18px, -50%);
|
||||
}
|
||||
|
||||
.checkbox input:focus-visible + .checkbox-ui {
|
||||
box-shadow: 0 0 0 4px var(--ui-focus);
|
||||
}
|
||||
|
||||
.dropPlaceholder {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dropPlaceholderColumn {
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.statischerTextInput {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.statischerTextInput:focus {
|
||||
border-color: var(--bg-top);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.columnTitleInput, .inputFontSize, .gebutsdatumFormatInput, .inputDisplayOnDisplayWidth {
|
||||
background: none;
|
||||
padding: 2px;
|
||||
margin-right: 10px;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
max-width: 20vw;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.columnTitleInput:focus {
|
||||
border-color: var(--bg-top);
|
||||
}
|
||||
|
||||
.customSelectAddOption .selectOptions {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.customSelectAddOption {
|
||||
position: absolute !important;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.inputFontSize {
|
||||
max-width: 60px;
|
||||
}
|
||||
|
||||
.inputDisplayOnDisplayWidth {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.autocomplete-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.verticalLayoutEl {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.verticalLayoutEl > span.dragableDropArea {
|
||||
min-height: 20px;
|
||||
min-width: max(50px, 100%);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.verticalLayoutEl > span.dragableDropArea:not(:last-child) {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.inputLabelWrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.selectSlugType {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.customSelectSlugType {
|
||||
border: 1px solid #000;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.selectCondEl.mainTable .singleValueSpan:not(.condEl) {
|
||||
border-color: #a5a5a5;
|
||||
color: #a5a5a5;
|
||||
background-color: #ececec;
|
||||
}
|
||||
|
||||
.selectCondEl.mainTable .singleValueSpan:not(.condEl):hover {
|
||||
border-color: #0fab91;
|
||||
color: #0fab91;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.selectCondEl.mainTable .singleValueSpan.selectedCondEl {
|
||||
border-color: #ab4b0f;
|
||||
color: #ab4b0f;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.blackBtn {
|
||||
background: var(--main-button);
|
||||
border: 1px solid #fff;
|
||||
color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
transition: transform 0.3s ease;
|
||||
letter-spacing: 1px;
|
||||
display: inline-flex;
|
||||
align-self: flex-start;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.blackBtn:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.blackBtn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,637 @@
|
||||
:root {
|
||||
--main: #fe3f18;
|
||||
--bg-top-raw: 254 63 24;
|
||||
--main-box-shadow: rgb(from var(--main) r g b / 0.05);
|
||||
--accent: #fe3f18;
|
||||
--accent-hover: #c5e300;
|
||||
--paddingSite: 40px;
|
||||
--card-radius: 20px;
|
||||
--card-bg: #ffffff;
|
||||
--bg: #FDFDFD;
|
||||
--card-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
|
||||
--border-subtle: #d4d7e1;
|
||||
--text-main: #191919;
|
||||
--text-muted: #5e5e5e;
|
||||
--main-button: #36454F;
|
||||
}
|
||||
|
||||
/* --- BASE & TYPOGRAPHY --- */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
background: var(--bg);
|
||||
color: var(--text-main);
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: none;
|
||||
font-optical-sizing: auto;
|
||||
}
|
||||
|
||||
/* --- LAYOUT WRAPPERS --- */
|
||||
.bgSection {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
margin-left: auto;
|
||||
box-sizing: border-box;
|
||||
background: none;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.bgSection.open {
|
||||
width: calc(100vw - 450px);
|
||||
}
|
||||
}
|
||||
|
||||
.menuTransition {
|
||||
transition: transform 0.45s cubic-bezier(.4, 0, .2, 1), width 0.45s cubic-bezier(.4, 0, .2, 1) !important;
|
||||
}
|
||||
|
||||
/* --- HEADER --- */
|
||||
.headerDivKampfrichter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--paddingSite);
|
||||
background: var(--main);
|
||||
color: #fff;
|
||||
margin-bottom: var(--paddingSite);
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.heading-pannel {
|
||||
margin: 0;
|
||||
font-weight: 300;
|
||||
font-size: 2rem;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.menuWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.mainContentDiv {
|
||||
padding: 0 var(--paddingSite);
|
||||
}
|
||||
|
||||
/* --- FOOTER / LOGOUT --- */
|
||||
.footerInternMenu {
|
||||
margin-top: auto;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
/* Burger + menu (unchanged from your style) */
|
||||
|
||||
.wk-leitungBurgerMenuDiv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
z-index: 99;
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
.wk-leitungBurgerMenuDiv svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
stroke: currentColor;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.wk-leitungBurgerMenuDiv.open svg {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.internMenuDiv {
|
||||
background-color: #fff;
|
||||
box-shadow: none;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100svh;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
transform: translateX(-100%);
|
||||
z-index: 100;
|
||||
padding: 30px;
|
||||
transition: transform 0.45s cubic-bezier(.4, 0, .2, 1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.internMenuDiv.open {
|
||||
transform: translateX(0);
|
||||
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.innerInternMenuDiv {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.text_akt_abt {
|
||||
font-size: 1rem;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.innerInternMenuDiv form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.innerInternMenuDiv input[type="submit"],
|
||||
.innerInternMenuDiv select {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.innerInternMenuDiv input[type="submit"]:focus,
|
||||
.innerInternMenuDiv select:focus {
|
||||
outline: none;
|
||||
border-color: #2d73ac;
|
||||
box-shadow: 0 0 6px rgba(45, 115, 172, 0.3);
|
||||
}
|
||||
|
||||
.innerInternMenuDiv .button-secondary {
|
||||
background: linear-gradient(135deg, #2d73ac, #1f4f75);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: 12px 18px;
|
||||
transition: transform 0.2s ease, background 0.3s ease;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.innerInternMenuDiv .button-secondary:hover {
|
||||
background: linear-gradient(135deg, #245d8a, #163b5a);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Riegeneinteilung Specific Styles */
|
||||
|
||||
.headerAbt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.headerAbt h2 {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
color: var(--main);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.textInputAbtTitel {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--main);
|
||||
font-size: 1.5rem;
|
||||
color: #191919;
|
||||
background: none;
|
||||
width: max-content;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.deleteProgramm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border: 1px solid #fee2e2;
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(244, 63, 94, 0.1);
|
||||
}
|
||||
|
||||
.deleteProgramm:hover {
|
||||
transform: scale(1.1);
|
||||
background: #fee2e2;
|
||||
box-shadow: 0 4px 8px rgba(244, 63, 94, 0.2);
|
||||
}
|
||||
|
||||
.deleteProgramm svg,
|
||||
.deleteProgramm path {
|
||||
fill: #f43f5e;
|
||||
}
|
||||
|
||||
#bin-lid {
|
||||
transform-origin: 50% 100%;
|
||||
transform-box: fill-box;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.deleteProgramm:hover #bin-lid {
|
||||
transform: rotate(20deg) translateY(-100px);
|
||||
}
|
||||
|
||||
/* Sidebar Menu Button Styling */
|
||||
.headerOptionConatiner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 20px;
|
||||
gap: 15px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.headerOptionConatiner > button {
|
||||
|
||||
}
|
||||
|
||||
.headerOptionConatiner label {
|
||||
font-weight: 400;
|
||||
color: var(--text-main);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
.headerOptionConatiner input[type="number"] {
|
||||
padding: 12px 15px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: #fff;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.headerOptionConatiner input[type="number"]:focus {
|
||||
border-color: var(--main);
|
||||
box-shadow: 0 0 0 3px rgba(40, 102, 110, 0.1);
|
||||
}
|
||||
|
||||
/* Custom Table / Drag and Drop Area */
|
||||
.geraet-table {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--card-shadow);
|
||||
min-width: 260px;
|
||||
overflow: hidden;
|
||||
margin-right: 15px;
|
||||
transition: all 0.3s ease;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.geraet-table thead {
|
||||
background: var(--main);
|
||||
}
|
||||
|
||||
.geraet-table thead th {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #fff;
|
||||
padding: 14px 20px;
|
||||
font-weight: 300;
|
||||
font-size: 1.05rem;
|
||||
text-align: left;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.geraet-table thead th i span {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.geraet-table tbody {
|
||||
display: block;
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior-y: contain;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for tbody */
|
||||
.geraet-table tbody::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.geraet-table tbody::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.geraet-table tbody::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.geraet-table tbody::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
.turnerin-row {
|
||||
cursor: grab;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease;
|
||||
display: block;
|
||||
border: 1px solid transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.turnerin-row:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.turnerin-row>td {
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: var(--text-main);
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.turnerin-row:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* Drag State */
|
||||
.dragging {
|
||||
opacity: 0.9 !important;
|
||||
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15) !important;
|
||||
transform: scale(1.03) rotate(1deg) !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.drop-placeholder {
|
||||
height: 54px;
|
||||
background: #f8fafc;
|
||||
border: 2px dashed #94a3b8;
|
||||
border-radius: 8px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.ui-sortable {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.empty-drop-row td {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Grouping Row Styling */
|
||||
.group-row {
|
||||
background: transparent;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.group-row>td {
|
||||
padding: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
padding: 12px 16px;
|
||||
cursor: grab;
|
||||
gap: 18px;
|
||||
border-radius: 10px;
|
||||
font-weight: 200;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.group-header:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.group-inner {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.group-row .group-inner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Invalid / Unassigned Wrapper */
|
||||
.invalidAbtDiv {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.invalidAbtDiv .geraet-table {
|
||||
box-shadow: none;
|
||||
border-color: #d93f4a;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invalidAbtDiv .geraet-table thead {
|
||||
background: #d93f4a;
|
||||
}
|
||||
|
||||
.invalidAbtDiv .geraet-table thead th {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
.singleAbtDiv {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.singleAbtDataContainer {
|
||||
overflow-x: auto;
|
||||
padding: 10px 0 20px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.singleAbtDataContainer::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.singleAbtDataContainer::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.singleAbtDataContainer::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.buttonMoveDiv {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.buttonMove {
|
||||
background: #fff;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
width: 24px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
input.abtIndexInput::-webkit-outer-spin-button,
|
||||
input.abtIndexInput::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input.abtIndexInput[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.abtIndexInput {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nichtBezahlt {
|
||||
color: #e11d48;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.timePicker {
|
||||
border: none;
|
||||
border-bottom: 1px solid #000;
|
||||
background: none;
|
||||
padding: 4px 8px;
|
||||
font-size: 1rem;
|
||||
color: var(--main);
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.addTimetypeButton {
|
||||
color: #2dac79;
|
||||
}
|
||||
|
||||
.removeTimetypeButton {
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.subHeaderAbt {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 4px 8px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
align-self: start;
|
||||
margin-top: 18px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.subHeaderAbt .customSelect {
|
||||
position: absolute;
|
||||
width: 160px;
|
||||
bottom: 0px;
|
||||
transform: translateY(120%) translateX(-10%);
|
||||
padding: 6px;
|
||||
background: #fff;
|
||||
border: 1px solid #7b7676;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.buttonsDiv {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 20px;
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.blackBtn {
|
||||
background: var(--main-button);
|
||||
border: 1px solid #fff;
|
||||
color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
transition: transform 0.3s ease;
|
||||
letter-spacing: 1px;
|
||||
display: flex;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.blackBtn:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.blackBtn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
h3.normalTitle {
|
||||
color: var(--main-button);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/* ─── Sidebar Navigation ─── */
|
||||
:root {
|
||||
--sidebar-width: 280px;
|
||||
--sidebar-bg: #111218;
|
||||
/* Dark navy / charcoal */
|
||||
--sidebar-text: #A0A0A0;
|
||||
/* Soft grey text */
|
||||
--sidebar-hover: rgba(255, 255, 255, 0.05);
|
||||
/* Subtile hover effect */
|
||||
--sidebar-active: rgba(255, 255, 255, 0.1);
|
||||
--sidebar-transition: 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Sidebar Panel */
|
||||
.sidebar-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--sidebar-width);
|
||||
height: 100vh;
|
||||
background: var(--sidebar-bg);
|
||||
z-index: 10000;
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--sidebar-transition), box-shadow var(--sidebar-transition);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sidebar-nav.open {
|
||||
transform: translateX(0);
|
||||
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* Sidebar Header */
|
||||
.sidebar-header {
|
||||
padding: 28px 24px 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.sidebar-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--sidebar-text);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-close-btn:hover {
|
||||
background: var(--sidebar-hover);
|
||||
}
|
||||
|
||||
/* Section Labels */
|
||||
.sidebar-section-label {
|
||||
padding: 20px 24px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.2px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
/* Links */
|
||||
.sidebar-links {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar-links a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 12px 24px;
|
||||
color: var(--sidebar-text);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.sidebar-links a:hover {
|
||||
background: var(--sidebar-hover);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.sidebar-links a.active {
|
||||
background: var(--sidebar-active);
|
||||
color: #FFFFFF;
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
|
||||
.sidebar-links a svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Overlay */
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--sidebar-transition);
|
||||
}
|
||||
|
||||
.sidebar-overlay.open {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Hamburger Toggle */
|
||||
.sidebar-toggle {
|
||||
background: #FFFFFF;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
transition: all var(--sidebar-transition);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-toggle span {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: #111;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-toggle.open span:nth-child(1) {
|
||||
transform: rotate(45deg) translate(5px, 5px);
|
||||
}
|
||||
|
||||
.sidebar-toggle.open span:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sidebar-toggle.open span:nth-child(3) {
|
||||
transform: rotate(-45deg) translate(5px, -5px);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.sidebar-footer {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.menuWrapper {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
border-radius: 40px;
|
||||
background-color: rgb(var(--bg-top-raw));
|
||||
}
|
||||
|
||||
.abmeldenbutton {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: linear-gradient(135deg, #e53935, #b71c1c);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, background 0.3s ease;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.abmeldenbutton:hover {
|
||||
background: linear-gradient(135deg, #c62828, #8e1919);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.sidebarUsername {
|
||||
padding: 20px 24px 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: rgb(146 146 146 / 85%);
|
||||
}
|
||||
|
||||
.customSelect {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.customSelect>* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.selectTrigger .selectArrow {
|
||||
transition: rotate 0.6s ease;
|
||||
}
|
||||
|
||||
table input {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.selectTriggerSidebar {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-nav .selectOptionsSidebar {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 4px;
|
||||
list-style: none;
|
||||
margin-top: 2px;
|
||||
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
|
||||
background: #fff;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
pointer-events: none;
|
||||
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-nav .customSelect.open .selectOptionsSidebar {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.customSelect.open .selectArrow {
|
||||
rotate: 180deg
|
||||
}
|
||||
|
||||
.selectOptionsSidebar li {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2px;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.selectOptionsSidebar li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ─── Sidebar Freigaben ─── */
|
||||
.sidebar-li-freigaben {
|
||||
padding: 0 24px 16px 42px;
|
||||
/* Indented under the icon of the active link */
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sidebar-freigaben-label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.selectTriggerSidebar {
|
||||
width: 100%;
|
||||
padding: 9px 12px;
|
||||
background: #171717;
|
||||
border: 1px solid #fcfcfc;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #fcfcfc;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.selectTriggerSidebar:hover, .sidebar-li-freigaben .customSelect.open .selectTriggerSidebar {
|
||||
background: #fcfcfc;
|
||||
color: #171717;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.selectOptionsSidebar li {
|
||||
font-size: 13px;
|
||||
padding: 8px 12px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.selectOptionsSidebar li.selected {
|
||||
background: var(--sidebar-active);
|
||||
color: #111;
|
||||
font-weight: 600;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
/* Dies ist eine CSS-Datei, welche mit eigener CSS-Logik gefüllt werden kann. Diese CSS Datei glit für alle Intern Seiten.*/
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 672 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 357 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,5 @@
|
||||
Alle Bilder für Intern müssen nach folgendem Schema benennt werden:
|
||||
|
||||
"Bg" + $typ ".webp"
|
||||
|
||||
für $typ gilt: _ sowie ucfirst
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user