New Filestructure for Docker

This commit is contained in:
2026-07-31 23:53:14 +02:00
parent 1e10ed4de8
commit 5bcf2a3546
239 changed files with 710 additions and 2000 deletions
BIN
View File
Binary file not shown.
+9
View File
@@ -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]
+30
View File
@@ -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]
+344
View File
@@ -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);
+22
View File
@@ -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";
+667
View File
@@ -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;
}
+315
View File
@@ -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;
}
}
+100
View File
@@ -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;
}
+320
View File
@@ -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;
}
+35
View File
@@ -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>
+39
View File
@@ -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>
+35
View File
@@ -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>
+35
View File
@@ -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>
+35
View File
@@ -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>
+104
View File
@@ -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;
}
+115
View File
@@ -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
}
});
+23
View File
@@ -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]
+322
View File
@@ -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>
+154
View File
@@ -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;
}
+402
View File
@@ -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;
}
+814
View File
@@ -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>
+25
View File
@@ -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.
Binary file not shown.
+91
View File
@@ -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 arent 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.
+15
View File
@@ -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.
BIN
View File
Binary file not shown.
+9
View File
@@ -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]
File diff suppressed because one or more lines are too long
+206
View File
@@ -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;
}
+92
View File
@@ -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;
}
+187
View File
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+147
View File
@@ -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
+120
View File
@@ -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;
}
+585
View File
@@ -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
+637
View File
@@ -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;
}
+341
View File
@@ -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
+1
View File
@@ -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

+5
View File
@@ -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