- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark, Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI) - aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource, an aether-core gekoppelt (Rechte über aether_users.admin_group), mit WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU) - Prototypen admin/ vervollständigt - SQL-Schema aether_admin.sql - Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
710 lines
39 KiB
HTML
710 lines
39 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="color-scheme" content="dark">
|
||
</head>
|
||
<body>
|
||
<title>Aether — Börse & Dokumente</title>
|
||
<style>
|
||
/* ===================== Tokens ===================== */
|
||
:root {
|
||
--page-bg: #0a0b10;
|
||
--fg: #e9ecf4;
|
||
--muted: #868da2;
|
||
--surface: rgba(24,26,36,0.7);
|
||
--surface2: rgba(38,41,55,0.8);
|
||
--border: rgba(255,255,255,0.09);
|
||
--accent: #c79a5a; /* Leder-Gold */
|
||
--red: #f0506e;
|
||
--green: #34d39a;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; height: 100%; }
|
||
body {
|
||
min-height: 100%;
|
||
background:
|
||
radial-gradient(1100px 700px at 15% 8%, rgba(199,154,90,0.08), transparent 55%),
|
||
radial-gradient(1000px 800px at 85% 92%, rgba(74,168,255,0.06), transparent 55%),
|
||
var(--page-bg);
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||
color: var(--fg);
|
||
display: flex; align-items: center; justify-content: center; padding: 32px 18px; overflow-x: hidden;
|
||
}
|
||
|
||
/* ===================== Börse (Wallet) ===================== */
|
||
.wallet {
|
||
position: relative; width: min(1080px, 96vw); aspect-ratio: 16 / 10;
|
||
border-radius: 22px; overflow: hidden; display: flex; flex-direction: column;
|
||
background:
|
||
linear-gradient(150deg, #3a2c1f, #241a12 60%, #1a130d),
|
||
var(--page-bg);
|
||
border: 1px solid rgba(199,154,90,0.28);
|
||
box-shadow: 0 50px 100px -30px rgba(0,0,0,0.85),
|
||
0 0 0 1px rgba(0,0,0,0.4), 0 2px 0 rgba(255,255,255,0.05) inset;
|
||
}
|
||
/* Ledernaht */
|
||
.wallet::before {
|
||
content: ""; position: absolute; inset: 10px; border-radius: 16px; pointer-events: none; z-index: 1;
|
||
border: 1.5px dashed rgba(199,154,90,0.35);
|
||
}
|
||
.wallet-head { position: relative; z-index: 2; display: flex; align-items: center; gap: 14px;
|
||
padding: 18px 26px; border-bottom: 1px solid rgba(199,154,90,0.2); }
|
||
.wallet-head .logo { width: 38px; height: 38px; border-radius: 10px; display: grid; place-items: center; font-size: 20px;
|
||
background: linear-gradient(150deg, #c79a5a, #8a6a34); box-shadow: 0 6px 16px rgba(199,154,90,0.4); }
|
||
.wallet-head h1 { font-size: 19px; margin: 0; font-weight: 800; letter-spacing: -0.02em; }
|
||
.wallet-head h1 small { display: block; font-size: 11.5px; font-weight: 500; color: #b79b74; letter-spacing: 0; }
|
||
.wallet-head .count { margin-left: auto; font-size: 12.5px; color: #b79b74; font-weight: 600; }
|
||
/* Börsen-Wechsel */
|
||
.wtabs { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
|
||
.wtab { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; padding: 7px 14px; border-radius: 11px;
|
||
background: rgba(0,0,0,0.25); border: 1px solid rgba(199,154,90,0.25); color: #cbb794; cursor: pointer; font-family: inherit; transition: .12s; }
|
||
.wtab:hover { border-color: var(--accent); }
|
||
.wtab.active { background: linear-gradient(150deg,#c79a5a,#8a6a34); color: #241a0c; border-color: transparent; }
|
||
.wtab .wn { font-size: 13px; font-weight: 800; }
|
||
.wtab .wc { font-size: 10.5px; font-weight: 600; opacity: 0.85; font-variant-numeric: tabular-nums; }
|
||
.wtab.buy { justify-content: center; align-items: center; color: var(--accent); border-style: dashed; font-weight: 800; font-size: 13px; }
|
||
.wtab.buy:hover { background: rgba(199,154,90,0.12); }
|
||
|
||
.wallet-body { position: relative; z-index: 2; flex: 1; display: grid; grid-template-columns: 268px 1fr; min-height: 0; }
|
||
.doclist { padding: 18px; overflow-y: auto; border-right: 1px solid rgba(199,154,90,0.18); display: flex; flex-direction: column; gap: 9px; }
|
||
.doclist::-webkit-scrollbar { width: 6px; } .doclist::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
|
||
.doctab { display: flex; align-items: center; gap: 12px; padding: 12px 13px; border-radius: 13px; cursor: pointer;
|
||
background: var(--surface); border: 1px solid var(--border); position: relative; overflow: hidden; transition: .14s; }
|
||
.doctab::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--dc, #888); }
|
||
.doctab:hover { background: var(--surface2); transform: translateX(2px); }
|
||
.doctab.active { border-color: var(--dc); box-shadow: 0 0 0 1px var(--dc) inset; }
|
||
.doctab .di { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; font-size: 17px; flex-shrink: 0;
|
||
background: rgba(255,255,255,0.06); }
|
||
.doctab .dn { font-size: 14px; font-weight: 700; }
|
||
.doctab .dm { font-size: 11.5px; color: var(--muted); }
|
||
|
||
.stage { padding: 26px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; min-height: 0; overflow-y: auto; }
|
||
.stage-actions { display: flex; gap: 10px; }
|
||
.btn { border: none; border-radius: 12px; padding: 12px 22px; font-size: 14.5px; font-weight: 700; cursor: pointer; font-family: inherit; transition: .12s; }
|
||
.btn.primary { background: var(--accent); color: #241a0c; }
|
||
.btn.primary:hover { filter: brightness(1.08); }
|
||
.btn.danger { background: var(--surface2); color: var(--red); border: 1px solid var(--border); }
|
||
.btn.danger:hover { background: rgba(240,80,110,0.14); }
|
||
.btn.ghost { background: var(--surface2); color: var(--fg); border: 1px solid var(--border); }
|
||
.btn:active { transform: scale(0.97); }
|
||
|
||
.empty { text-align: center; color: var(--muted); }
|
||
.empty .big { font-size: 52px; margin-bottom: 10px; }
|
||
|
||
/* ===================== Dokument-Basis (ID-1 Format) ===================== */
|
||
.doccard {
|
||
width: 100%; max-width: 480px; aspect-ratio: 1.586 / 1; border-radius: 16px; position: relative;
|
||
overflow: hidden; color: #10141a; box-shadow: 0 30px 60px -20px rgba(0,0,0,0.7), 0 0 0 1px rgba(0,0,0,0.3);
|
||
font-size: 13px; user-select: none;
|
||
}
|
||
.doccard .foto { border-radius: 8px; background: linear-gradient(160deg,#c9d2dc,#8b97a6); overflow: hidden;
|
||
display: grid; place-items: end center; }
|
||
.doccard .foto svg { width: 100%; height: 100%; }
|
||
.lbl { font-size: 8px; letter-spacing: 0.05em; text-transform: uppercase; opacity: 0.7; font-weight: 700; }
|
||
.val { font-size: 12.5px; font-weight: 700; margin-top: 1px; }
|
||
.mono { font-family: ui-monospace, "SF Mono", monospace; letter-spacing: 0.06em; }
|
||
/* Hologramm-Schimmer */
|
||
.holo { position: absolute; inset: 0; pointer-events: none; mix-blend-mode: overlay; opacity: 0.5;
|
||
background: linear-gradient(115deg, transparent 30%, rgba(255,255,255,0.5) 45%, transparent 60%);
|
||
background-size: 300% 300%; animation: holo 5s linear infinite; }
|
||
@keyframes holo { 0% { background-position: 120% 0; } 100% { background-position: -120% 0; } }
|
||
@media (prefers-reduced-motion: reduce) { .holo { animation: none; opacity: 0.25; } }
|
||
|
||
/* ---- Personalausweis ---- */
|
||
.doc-perso {
|
||
color: #16241d;
|
||
background:
|
||
repeating-linear-gradient(60deg, rgba(255,255,255,0.05) 0 2px, transparent 2px 6px),
|
||
repeating-linear-gradient(-60deg, rgba(0,80,50,0.05) 0 2px, transparent 2px 7px),
|
||
linear-gradient(150deg, #e6efe6, #cfe3d6 55%, #bcd9c8);
|
||
}
|
||
.doc-perso .top { display: flex; justify-content: space-between; align-items: flex-start; padding: 12px 16px 0; }
|
||
.doc-perso .title { font-size: 15px; font-weight: 800; letter-spacing: 0.02em; color: #0f4d36; }
|
||
.doc-perso .sub { font-size: 8.5px; letter-spacing: 0.18em; color: #2a6b52; font-weight: 700; }
|
||
.doc-perso .body { display: grid; grid-template-columns: 92px 1fr; gap: 14px; padding: 10px 16px; }
|
||
.doc-perso .foto { height: 116px; }
|
||
.doc-perso .fields { display: grid; grid-template-columns: 1fr 1fr; gap: 7px 12px; align-content: start; }
|
||
.doc-perso .mrz { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(255,255,255,0.55);
|
||
font-family: ui-monospace, monospace; font-size: 10px; letter-spacing: 0.14em; padding: 4px 16px; color: #223; border-top: 1px solid rgba(0,0,0,0.1); }
|
||
.doc-perso .emblem { width: 34px; height: 34px; }
|
||
|
||
/* ---- Führerschein ---- */
|
||
.doc-fs { color: #2a1420; background: linear-gradient(150deg, #f7dfe6, #efc3d2 55%, #e7a9bf); }
|
||
.doc-fs .eu { position: absolute; top: 12px; right: 14px; width: 44px; height: 44px; border-radius: 50%;
|
||
background: radial-gradient(circle, #1a3a8a, #0f245e); display: grid; place-items: center; }
|
||
.doc-fs .top { padding: 12px 16px 0; }
|
||
.doc-fs .title { font-size: 15px; font-weight: 800; color: #9a1b46; letter-spacing: 0.03em; }
|
||
.doc-fs .sub { font-size: 8.5px; letter-spacing: 0.16em; color: #b24a6c; font-weight: 700; }
|
||
.doc-fs .body { display: grid; grid-template-columns: 88px 1fr; gap: 14px; padding: 10px 16px 6px; }
|
||
.doc-fs .foto { height: 104px; background: linear-gradient(160deg,#e7c7d2,#c99cad); }
|
||
.doc-fs .fields { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px; align-content: start; }
|
||
.doc-fs .klassen { position: absolute; bottom: 10px; left: 16px; right: 16px; display: flex; gap: 6px; align-items: center; }
|
||
.doc-fs .kl { background: #9a1b46; color: #fff; font-weight: 800; font-size: 11px; padding: 3px 9px; border-radius: 6px; }
|
||
|
||
/* ---- Waffenschein ---- */
|
||
.doc-ws { color: #f0e6d8; background: linear-gradient(155deg, #241a1a, #17110f 60%, #0f0b0a); display: flex; flex-direction: column; }
|
||
.doc-ws .band { position: absolute; top: 0; left: 0; right: 0; height: 5px; background: linear-gradient(90deg,#8a1f1f,#e0503f,#8a1f1f); }
|
||
.doc-ws .top { padding: 14px 18px 2px; display: flex; justify-content: space-between; align-items: flex-start; }
|
||
.doc-ws .title { font-size: 16px; font-weight: 800; letter-spacing: 0.05em; color: #e0503f; }
|
||
.doc-ws .sub { font-size: 8.5px; letter-spacing: 0.14em; color: #b98f6a; font-weight: 700; }
|
||
.doc-ws .ws-info { display: flex; gap: 16px; flex-wrap: wrap; padding: 4px 18px 8px; font-size: 11px; }
|
||
.doc-ws .ws-info b { font-size: 12px; }
|
||
.doc-ws .wtable { margin: 0 18px; }
|
||
.doc-ws .wrow { display: grid; grid-template-columns: 1.5fr 0.7fr 1.2fr; gap: 8px; font-size: 10.5px; padding: 3.5px 0; border-top: 1px solid rgba(255,255,255,0.12); }
|
||
.doc-ws .wrow.h { color: #b98f6a; font-weight: 800; text-transform: uppercase; font-size: 8px; letter-spacing: 0.06em; border: none; }
|
||
.doc-ws .stamp { position: absolute; right: 14px; bottom: 12px; width: 66px; height: 66px; border-radius: 50%;
|
||
border: 2px solid rgba(224,80,63,0.5); color: rgba(224,80,63,0.65); display: grid; place-items: center; text-align: center;
|
||
font-size: 8px; font-weight: 800; transform: rotate(-14deg); line-height: 1.3; letter-spacing: 0.04em; opacity: 0.75; }
|
||
|
||
/* Visitenkarte: nur Handykontakt (Name + Nummer) */
|
||
.doc-vk .vk-tag { font-size: 10px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--muted); font-weight: 700; }
|
||
.doc-vk .vk-name { font-size: 30px; font-weight: 800; letter-spacing: -0.02em; margin-top: 6px; }
|
||
.doc-vk .vk-phone { color: #f5a623; font-weight: 800; font-size: 23px; margin-top: 10px; font-variant-numeric: tabular-nums; letter-spacing: 0.03em; }
|
||
|
||
/* ---- Gesundheitskarte ---- */
|
||
.doc-gk { color: #08343f; background: linear-gradient(150deg, #d6f0f2, #a9dfe6 55%, #7fcdd8); }
|
||
.doc-gk .top { padding: 14px 18px 0; display: flex; justify-content: space-between; align-items: center; }
|
||
.doc-gk .title { font-size: 14px; font-weight: 800; color: #0b6b7a; letter-spacing: 0.02em; }
|
||
.doc-gk .cross { width: 30px; height: 30px; border-radius: 7px; background: #e2413f; display: grid; place-items: center; color: #fff; font-weight: 900; font-size: 20px; line-height: 1; }
|
||
.doc-gk .chip { width: 44px; height: 34px; border-radius: 6px; margin: 14px 0 0 18px;
|
||
background: linear-gradient(135deg,#e9d9a0,#b89b53); position: relative; }
|
||
.doc-gk .chip::after { content:""; position:absolute; inset:6px; border:1px solid rgba(0,0,0,0.25); border-radius:3px; }
|
||
.doc-gk .body { padding: 8px 18px; display: grid; grid-template-columns: 1fr 1fr; gap: 6px 14px; }
|
||
.doc-gk .insn { position: absolute; bottom: 12px; left: 18px; font-size: 15px; font-weight: 800; letter-spacing: 0.02em; color: #0b6b7a; }
|
||
|
||
/* ---- Visitenkarte ---- */
|
||
.doc-vk { color: #eef1f6; background: linear-gradient(150deg, #1c2333, #12161f 70%); border: 1px solid rgba(245,166,35,0.3); }
|
||
.doc-vk .accentbar { position: absolute; left: 0; top: 0; bottom: 0; width: 8px; background: linear-gradient(180deg,#f5a623,#c9791a); }
|
||
.doc-vk .vk-in { padding: 22px 22px 22px 30px; height: 100%; display: flex; flex-direction: column; justify-content: center; }
|
||
.doc-vk .vk-name { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; }
|
||
.doc-vk .vk-role { color: #f5a623; font-weight: 700; font-size: 13px; margin-top: 2px; letter-spacing: 0.04em; }
|
||
.doc-vk .vk-firma { color: var(--muted); font-size: 12px; margin-top: 2px; }
|
||
.doc-vk .vk-lines { margin-top: 16px; display: grid; gap: 5px; font-size: 12.5px; }
|
||
.doc-vk .vk-lines div { display: flex; align-items: center; gap: 9px; }
|
||
.doc-vk .vk-lines .ic { opacity: 0.8; }
|
||
.doc-vk .vk-logo { position: absolute; top: 20px; right: 22px; width: 44px; height: 44px; border-radius: 11px;
|
||
background: linear-gradient(150deg,#f5a623,#c9791a); display: grid; place-items: center; color: #241a06; font-weight: 900; font-size: 20px; }
|
||
|
||
/* ===================== Overlays ===================== */
|
||
.overlay { position: absolute; inset: 0; z-index: 30; background: rgba(6,7,11,0.72); backdrop-filter: blur(6px);
|
||
display: none; align-items: center; justify-content: center; flex-direction: column; gap: 20px; padding: 30px; }
|
||
.overlay.show { display: flex; }
|
||
.present-caption { text-align: center; color: #dfe4ee; }
|
||
.present-caption .pc-h { font-size: 17px; font-weight: 800; }
|
||
.present-caption .pc-s { font-size: 13px; color: var(--muted); margin-top: 3px; }
|
||
.present-card { transform: scale(1.18); filter: drop-shadow(0 30px 60px rgba(0,0,0,0.6)); animation: raise .4s ease; }
|
||
@keyframes raise { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1.18); opacity: 1; } }
|
||
.show-to { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; max-width: 520px; }
|
||
.show-to button { display: flex; align-items: center; gap: 8px; background: var(--surface2); border: 1px solid var(--border);
|
||
color: var(--fg); border-radius: 999px; padding: 8px 14px; font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; }
|
||
.show-to button:hover { border-color: var(--accent); }
|
||
.show-to .pa { width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center; font-size: 11px; font-weight: 800; color: #fff; }
|
||
|
||
.modal { width: 380px; max-width: 92%; background: rgba(18,20,28,0.98); border: 1px solid var(--border);
|
||
border-radius: 18px; padding: 24px; text-align: center; box-shadow: 0 30px 70px -18px rgba(0,0,0,0.8); }
|
||
.modal .mic { font-size: 40px; }
|
||
.modal h3 { margin: 8px 0 4px; font-size: 19px; }
|
||
.modal p { margin: 0 0 20px; color: var(--muted); font-size: 13.5px; line-height: 1.5; }
|
||
.modal-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||
|
||
.toast { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%) translateY(18px);
|
||
background: rgba(12,14,20,0.97); border: 1px solid var(--border); color: var(--fg); padding: 11px 20px;
|
||
border-radius: 13px; font-size: 13.5px; font-weight: 600; z-index: 40; opacity: 0; pointer-events: none; transition: .3s; }
|
||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||
|
||
.hint-bar { position: fixed; bottom: 14px; left: 50%; transform: translateX(-50%); z-index: 5; font-size: 12px; color: #6f7789; }
|
||
.hint-bar b { color: #9aa2b4; }
|
||
</style>
|
||
|
||
<div class="wallet" id="wallet">
|
||
<div class="wallet-head">
|
||
<span class="logo">👛</span>
|
||
<h1>Börse<small>Max Weber · Dokumente & Karten</small></h1>
|
||
<div class="wtabs" id="wtabs"></div>
|
||
</div>
|
||
<div class="wallet-body">
|
||
<div class="doclist" id="doclist"></div>
|
||
<div class="stage" id="stage"></div>
|
||
</div>
|
||
|
||
<div class="overlay" id="overlay"></div>
|
||
<div class="toast" id="toast"></div>
|
||
</div>
|
||
|
||
<div class="hint-bar"><b>Karte wählen</b> · <b>Vorzeigen</b> präsentiert das Dokument · <b>Verlieren</b> entfernt es aus der Börse</div>
|
||
|
||
<script>
|
||
"use strict";
|
||
|
||
/* =====================================================================
|
||
PERSON — Stammdaten des Charakters (später aus dem Charakter-Datensatz)
|
||
===================================================================== */
|
||
/* Personendaten. In FiveM vom Server befüllt (Name/Geburtsdatum kommen
|
||
live aus dem Charakter, die Dokumentnummern aus der Datenbank). */
|
||
let person = {
|
||
vorname: "Max", nachname: "Weber", name: "Max Weber",
|
||
geb: "14.03.1996", gebOrt: "Los Santos", nationalitaet: "San Andreas",
|
||
geschlecht: "männlich", groesse: "182 cm", augen: "braun",
|
||
adresse: "Vespucci Blvd 4B",
|
||
persoNr: "LSA-2847-1196", persoGueltig: "14.03.2034",
|
||
fsNr: "DL-77 4821 SA", fsKlassen: ["A", "B", "C1"], fsAusstellung: "2016", fsGueltig: "2032",
|
||
wsNr: "WA-1102-LS", wsGueltig: "31.12.2027",
|
||
wsWaffen: [
|
||
{ art: "Pistole P99", kaliber: "9 mm", serie: "P99-8842-LS" },
|
||
{ art: "Glock 19", kaliber: "9 mm", serie: "GL19-3391-LS" },
|
||
{ art: "MP5 (MPX)", kaliber: "9 mm", serie: "MP5-0075-LS" },
|
||
{ art: "Schrotflinte 870", kaliber: "12/70", serie: "SG12-2210-LS" }
|
||
],
|
||
gkVersicherung: "San Andreas Gesundheit", gkNr: "A123 456 789", gkGueltig: "12/28",
|
||
telefon: "555-0182"
|
||
};
|
||
|
||
/* Personen in der Nähe (zum "Vorzeigen").
|
||
Der Server schickt bewusst keine Namen mit — im Rollenspiel kennt man
|
||
eine Person erst nach Ausweis oder Visitenkarte. */
|
||
let spielerNah = [];
|
||
|
||
/* =====================================================================
|
||
DOKUMENTE — jedes ist ein einzelnes Item mit eigenem Kartendesign
|
||
===================================================================== */
|
||
/* Dokumente des Spielers — in FiveM vom Server geladen.
|
||
Jedes trägt seine Datenbank-ID mit, damit Aktionen zuordenbar sind. */
|
||
let dokumente = [
|
||
{ id: "perso", name: "Personalausweis", icon: "🪪", accent: "#2f9e73", info: "gültig bis " + person.persoGueltig },
|
||
{ id: "fs", name: "Führerschein", icon: "🚗", accent: "#d5638a", info: "Klassen " + person.fsKlassen.join(", ") },
|
||
{ id: "ws", name: "Waffenschein", icon: "🔫", accent: "#e0503f", info: "gültig bis " + person.wsGueltig },
|
||
{ id: "gk", name: "Gesundheitskarte", icon: "➕", accent: "#2fb0c4", info: person.gkVersicherung },
|
||
{ id: "vk", name: "Visitenkarte", icon: "💼", accent: "#f5a623", info: person.telefon }
|
||
];
|
||
|
||
/* Mehrere Börsen möglich (jede Börse ist ein Item im Inventar).
|
||
Jede fasst max. 10 Dokumente; ein Dokument liegt in genau einer Börse. */
|
||
let MAX_SLOTS = 10;
|
||
let boersen = [
|
||
{ name: "Börse 1", docs: dokumente.map(d => d.id) },
|
||
{ name: "Börse 2", docs: [] }
|
||
];
|
||
let aktiveBoerse = 0; // aktuell geöffnete Börse
|
||
let aktiv = boersen[0].docs[0] || null; // aktuell ausgewähltes Dokument
|
||
const akt = () => boersen[aktiveBoerse]; // aktuelle Börse
|
||
|
||
/* =====================================================================
|
||
HILFSFUNKTIONEN
|
||
===================================================================== */
|
||
const $ = id => document.getElementById(id);
|
||
function sicher(t) { const d = document.createElement("div"); d.textContent = t; return d.innerHTML; }
|
||
let toastTimer = null;
|
||
function toast(text) { const t = $("toast"); t.textContent = text; t.classList.add("show"); clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.remove("show"), 2200); }
|
||
// wert darf HTML enthalten (z. B. <span class="mono">) — Werte stammen aus vertrauenswürdigen Stammdaten
|
||
const feld = (label, wert) => '<div><div class="lbl">' + label + '</div><div class="val">' + wert + '</div></div>';
|
||
|
||
// Personen-Silhouette (Passfoto)
|
||
const fotoSvg = '<svg viewBox="0 0 100 120" preserveAspectRatio="xMidYMax meet"><circle cx="50" cy="42" r="24" fill="#5a6472"/><path d="M12 120 q0 -34 38 -34 q38 0 38 34 Z" fill="#5a6472"/></svg>';
|
||
// Sternenkreis (EU-Stil) für den Führerschein
|
||
function sternRing() {
|
||
let s = ""; for (let i = 0; i < 12; i++) { const a = i / 12 * 2 * Math.PI - Math.PI / 2; s += '<text x="' + (22 + 15 * Math.cos(a)).toFixed(1) + '" y="' + (26 + 15 * Math.sin(a)).toFixed(1) + '" font-size="6" fill="#f5c542" text-anchor="middle">★</text>'; }
|
||
return '<svg width="44" height="44" viewBox="0 0 44 44">' + s + '<text x="22" y="25" font-size="9" fill="#f5c542" text-anchor="middle" font-weight="800">SA</text></svg>';
|
||
}
|
||
|
||
/* =====================================================================
|
||
KARTEN-RENDERER — jedes Dokument sieht anders aus
|
||
===================================================================== */
|
||
const kartenRenderer = {
|
||
perso: () =>
|
||
'<div class="doccard doc-perso">' +
|
||
'<div class="holo"></div>' +
|
||
'<div class="top"><div><div class="title">PERSONALAUSWEIS</div><div class="sub">REPUBLIK SAN ANDREAS · IDENTITY CARD</div></div>' +
|
||
'<svg class="emblem" viewBox="0 0 34 34"><circle cx="17" cy="17" r="16" fill="none" stroke="#0f4d36" stroke-width="1.5"/><path d="M17 5 l3 8 h8 l-6 5 2 9 -7 -5 -7 5 2 -9 -6 -5 h8 Z" fill="#0f4d36"/></svg></div>' +
|
||
'<div class="body">' +
|
||
'<div class="foto">' + fotoSvg + '</div>' +
|
||
'<div class="fields">' +
|
||
feld("Name", person.nachname) + feld("Vornamen", person.vorname) +
|
||
feld("Geburtsdatum", person.geb) + feld("Geburtsort", person.gebOrt) +
|
||
feld("Staatsangeh.", person.nationalitaet) + feld("Größe", person.groesse) +
|
||
'<div style="grid-column:span 2">' + feld("Ausweisnr.", '<span class="mono">' + person.persoNr + '</span> · gültig bis ' + person.persoGueltig) + '</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="mrz">IDSAA' + person.persoNr.replace(/[^0-9]/g, "") + '<<<<<<<<<<<br>9603140M3403147SAA<<WEBER<<MAX<<<<<<<<</div>' +
|
||
'</div>',
|
||
|
||
fs: () =>
|
||
'<div class="doccard doc-fs">' +
|
||
'<div class="holo"></div>' +
|
||
'<div class="eu">' + sternRing() + '</div>' +
|
||
'<div class="top"><div class="title">FÜHRERSCHEIN</div><div class="sub">REPUBLIK SAN ANDREAS · DRIVING LICENCE</div></div>' +
|
||
'<div class="body">' +
|
||
'<div class="foto">' + fotoSvg + '</div>' +
|
||
'<div class="fields">' +
|
||
feld("1. Name", person.nachname) + feld("2. Vorname", person.vorname) +
|
||
feld("3. Geburtsdatum", person.geb) + feld("4a. Ausstellung", person.fsAusstellung) +
|
||
feld("4b. Gültig bis", person.fsGueltig) + feld("5. Nr.", '<span class="mono">' + person.fsNr + '</span>') +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="klassen"><span class="lbl" style="color:#9a1b46">9. Klassen:</span>' +
|
||
person.fsKlassen.map(k => '<span class="kl">' + k + '</span>').join("") + '</div>' +
|
||
'</div>',
|
||
|
||
ws: () =>
|
||
'<div class="doccard doc-ws">' +
|
||
'<div class="band"></div>' +
|
||
'<div class="stamp">GEPRÜFT<br>LSPD<br>★ SA ★</div>' +
|
||
'<div class="top"><div><div class="title">WAFFENSCHEIN</div><div class="sub">INNENMINISTERIUM SAN ANDREAS</div></div>' +
|
||
'<div style="text-align:right"><div class="lbl" style="color:#b98f6a">Schein-Nr.</div><div class="val mono">' + person.wsNr + '</div></div></div>' +
|
||
'<div class="ws-info">' +
|
||
'<span><span class="lbl">Inhaber</span> <b>' + sicher(person.name) + '</b></span>' +
|
||
'<span><span class="lbl">Geboren</span> <b>' + sicher(person.geb) + '</b></span>' +
|
||
'<span><span class="lbl">Gültig bis</span> <b>' + sicher(person.wsGueltig) + '</b></span>' +
|
||
'</div>' +
|
||
'<div class="wtable">' +
|
||
'<div class="wrow h"><span>Registrierte Waffe (' + person.wsWaffen.length + ')</span><span>Kaliber</span><span>Seriennummer</span></div>' +
|
||
person.wsWaffen.map(w => '<div class="wrow"><span>' + sicher(w.art) + '</span><span>' + sicher(w.kaliber) + '</span><span class="mono">' + sicher(w.serie) + '</span></div>').join("") +
|
||
'</div>' +
|
||
'</div>',
|
||
|
||
gk: () =>
|
||
'<div class="doccard doc-gk">' +
|
||
'<div class="holo"></div>' +
|
||
'<div class="top"><div class="title">GESUNDHEITSKARTE</div><div class="cross">+</div></div>' +
|
||
'<div class="chip"></div>' +
|
||
'<div class="body" style="margin-top:6px">' +
|
||
feld("Versicherte/r", person.name) + feld("Vers.-Nr.", '<span class="mono">' + person.gkNr + '</span>') +
|
||
feld("Geboren", person.geb) + feld("Gültig", person.gkGueltig) +
|
||
'</div>' +
|
||
'<div class="insn">' + sicher(person.gkVersicherung) + '</div>' +
|
||
'</div>',
|
||
|
||
vk: () =>
|
||
'<div class="doccard doc-vk">' +
|
||
'<div class="accentbar"></div>' +
|
||
'<div class="vk-logo">📱</div>' +
|
||
'<div class="vk-in">' +
|
||
'<div class="vk-tag">Handykontakt</div>' +
|
||
'<div class="vk-name">' + sicher(person.name) + '</div>' +
|
||
'<div class="vk-phone">' + sicher(person.telefon) + '</div>' +
|
||
'</div>' +
|
||
'</div>'
|
||
};
|
||
|
||
function renderKarte(id) { return kartenRenderer[id] ? kartenRenderer[id]() : ""; }
|
||
|
||
/* =====================================================================
|
||
BÖRSE RENDERN
|
||
===================================================================== */
|
||
// Börsen-Wechsel-Leiste (Tabs) inkl. Shop-Kauf
|
||
function renderBoersenTabs() {
|
||
const wt = $("wtabs");
|
||
wt.innerHTML = boersen.map((b, i) =>
|
||
'<button class="wtab' + (i === aktiveBoerse ? " active" : "") + '" data-b="' + i + '">' +
|
||
'<span class="wn">' + sicher(b.name) + '</span><span class="wc">' + b.docs.length + " / " + MAX_SLOTS + '</span></button>').join("") +
|
||
'<button class="wtab buy" id="buy-boerse" title="Weitere Börse im Shop kaufen">+ Börse</button>';
|
||
wt.querySelectorAll("[data-b]").forEach(t => t.addEventListener("click", () => {
|
||
aktiveBoerse = +t.dataset.b; aktiv = akt().docs[0] || null; render();
|
||
}));
|
||
$("buy-boerse").addEventListener("click", kaufeBoerse);
|
||
}
|
||
|
||
// Neue Börse "kaufen" (Shop-Demo)
|
||
function kaufeBoerse() {
|
||
if (IST_FIVEM) {
|
||
// Börsen sind Items — sie werden im 24/7 gekauft, nicht hier erzeugt
|
||
toast("Börsen bekommst du im 24/7-Laden");
|
||
return;
|
||
}
|
||
if (boersen.length >= 4) { toast("Maximale Anzahl Börsen erreicht"); return; }
|
||
boersen.push({ name: "Börse " + (boersen.length + 1), docs: [] });
|
||
aktiveBoerse = boersen.length - 1; aktiv = null;
|
||
toast("Neue Börse gekauft");
|
||
render();
|
||
}
|
||
|
||
function render() {
|
||
renderBoersenTabs();
|
||
const besitz = akt().docs;
|
||
const liste = $("doclist");
|
||
|
||
if (besitz.length === 0) {
|
||
liste.innerHTML = '<div class="empty" style="margin:auto"><div class="big">👛</div><div>' + sicher(akt().name) + ' ist leer.</div>' +
|
||
'<div style="font-size:12px;margin-top:6px">Dokumente aus einer<br>anderen Börse hierher verschieben.</div></div>';
|
||
$("stage").innerHTML = '<div class="empty"><div class="big">📭</div><div style="font-weight:700;color:var(--fg)">Keine Dokumente in ' + sicher(akt().name) + '</div></div>';
|
||
return;
|
||
}
|
||
liste.innerHTML = besitz.map(id => {
|
||
const d = dokumente.find(x => x.id === id);
|
||
return '<div class="doctab' + (id === aktiv ? " active" : "") + '" data-doc="' + id + '" style="--dc:' + d.accent + '">' +
|
||
'<span class="di" style="color:' + d.accent + '">' + d.icon + '</span>' +
|
||
'<div><div class="dn">' + sicher(d.name) + '</div><div class="dm">' + sicher(d.info) + '</div></div></div>';
|
||
}).join("");
|
||
liste.querySelectorAll("[data-doc]").forEach(t => t.addEventListener("click", () => { aktiv = t.dataset.doc; render(); }));
|
||
|
||
// Bühne rechts: aktive Karte + Aktionen
|
||
if (!besitz.includes(aktiv)) aktiv = besitz[0];
|
||
$("stage").innerHTML = renderKarte(aktiv) +
|
||
'<div class="stage-actions">' +
|
||
'<button class="btn primary" id="act-show">Vorzeigen</button>' +
|
||
(boersen.length > 1 ? '<button class="btn ghost" id="act-move">Verschieben</button>' : "") +
|
||
'<button class="btn danger" id="act-lose">Verlieren</button>' +
|
||
'</div>';
|
||
$("act-show").addEventListener("click", vorzeigen);
|
||
if ($("act-move")) $("act-move").addEventListener("click", verschiebenFrage);
|
||
$("act-lose").addEventListener("click", verlierenFrage);
|
||
}
|
||
|
||
/* Dokument in eine andere Börse verschieben (Ziel muss freien Platz haben) */
|
||
function verschiebenFrage() {
|
||
const d = dokumente.find(x => x.id === aktiv);
|
||
const ziele = boersen.map((b, i) => ({ b, i })).filter(o => o.i !== aktiveBoerse && o.b.docs.length < MAX_SLOTS);
|
||
if (ziele.length === 0) { toast("Keine andere Börse mit freiem Platz"); return; }
|
||
$("overlay").innerHTML =
|
||
'<div class="modal"><div class="mic">📂</div><h3>' + sicher(d.name) + ' verschieben</h3>' +
|
||
'<p>In welche Börse soll das Dokument?</p>' +
|
||
'<div style="display:grid;gap:8px">' + ziele.map(o =>
|
||
'<button class="btn ghost" data-t="' + o.i + '" style="width:100%;display:flex;justify-content:space-between">' +
|
||
sicher(o.b.name) + '<span style="opacity:0.7">' + o.b.docs.length + " / " + MAX_SLOTS + '</span></button>').join("") +
|
||
'</div><div style="height:10px"></div><button class="btn ghost" id="m-cancel" style="width:100%">Abbrechen</button></div>';
|
||
$("overlay").classList.add("show");
|
||
$("m-cancel").addEventListener("click", schliesseOverlay);
|
||
$("overlay").querySelectorAll("[data-t]").forEach(b => b.addEventListener("click", () => {
|
||
const zi = +b.dataset.t;
|
||
schliesseOverlay();
|
||
if (IST_FIVEM) { nui("verschieben", { dokumentId: d.dbId, zielBoerse: zi }); return; }
|
||
// Browser-Ansicht: lokal verschieben
|
||
akt().docs = akt().docs.filter(id => id !== aktiv);
|
||
boersen[zi].docs.push(aktiv);
|
||
toast(d.name + " → " + boersen[zi].name);
|
||
aktiv = akt().docs[0] || null;
|
||
render();
|
||
}));
|
||
}
|
||
|
||
/* =====================================================================
|
||
VORZEIGEN — Dokument präsentieren (optional gezielt an Spieler)
|
||
===================================================================== */
|
||
function vorzeigen() {
|
||
const d = dokumente.find(x => x.id === aktiv);
|
||
$("overlay").innerHTML =
|
||
'<div class="present-caption"><div class="pc-h">Du zeigst deinen ' + sicher(d.name) + ' vor</div>' +
|
||
'<div class="pc-s">Halte ihn hoch — oder wähle gezielt eine Person</div></div>' +
|
||
'<div class="present-card">' + renderKarte(aktiv) + '</div>' +
|
||
'<div class="show-to" id="show-to">' +
|
||
spielerNah.map((sp, i) => '<button data-p="' + i + '"><span class="pa" style="background:' + sp.farbe + '">' + (sp.bekannt ? sp.name.substring(0, 1) : "?") + '</span>' +
|
||
(sp.bekannt ? sicher(sp.name) : "Unbekannt #" + sp.id) + '</button>').join("") +
|
||
'</div>' +
|
||
'<button class="btn ghost" id="present-close">Einstecken</button>';
|
||
$("overlay").classList.add("show");
|
||
$("present-close").addEventListener("click", schliesseOverlay);
|
||
$("show-to").querySelectorAll("[data-p]").forEach(b => b.addEventListener("click", () => {
|
||
const sp = spielerNah[+b.dataset.p];
|
||
schliesseOverlay();
|
||
if (IST_FIVEM) { nui("vorzeigen", { dokumentId: d.dbId, zielId: sp.id }); return; }
|
||
toast(d.name + " vorgezeigt an Unbekannt #" + sp.id);
|
||
}));
|
||
}
|
||
|
||
/* =====================================================================
|
||
VERLIEREN — Dokument aus der Börse entfernen
|
||
===================================================================== */
|
||
function verlierenFrage() {
|
||
const d = dokumente.find(x => x.id === aktiv);
|
||
$("overlay").innerHTML =
|
||
'<div class="modal"><div class="mic">⚠️</div><h3>' + sicher(d.name) + ' verlieren?</h3>' +
|
||
'<p>Das Dokument wird aus deiner Börse entfernt. Im Spiel müsstest du es neu beantragen.</p>' +
|
||
'<div class="modal-actions"><button class="btn ghost" id="v-cancel">Abbrechen</button>' +
|
||
'<button class="btn danger" id="v-ok">Verlieren</button></div></div>';
|
||
$("overlay").classList.add("show");
|
||
$("v-cancel").addEventListener("click", schliesseOverlay);
|
||
$("v-ok").addEventListener("click", () => {
|
||
schliesseOverlay();
|
||
if (IST_FIVEM) { nui("verlieren", { dokumentId: d.dbId }); return; }
|
||
akt().docs = akt().docs.filter(id => id !== aktiv);
|
||
toast("Du hast deinen " + d.name + " verloren");
|
||
aktiv = akt().docs[0] || null;
|
||
render();
|
||
});
|
||
}
|
||
|
||
function schliesseOverlay() { $("overlay").classList.remove("show"); $("overlay").innerHTML = ""; }
|
||
$("overlay").addEventListener("click", e => { if (e.target === $("overlay")) schliesseOverlay(); });
|
||
|
||
/* =====================================================================
|
||
NUI-BRÜCKE — Verbindung zum FiveM-Client
|
||
===================================================================== */
|
||
const IST_FIVEM = (typeof GetParentResourceName === "function");
|
||
const RESSOURCE = IST_FIVEM ? GetParentResourceName() : "aether-documents";
|
||
|
||
function nui(aktion, daten) {
|
||
if (!IST_FIVEM) return Promise.resolve();
|
||
return fetch(`https://${RESSOURCE}/${aktion}`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json; charset=UTF-8" },
|
||
body: JSON.stringify(daten || {}),
|
||
}).catch(() => {});
|
||
}
|
||
|
||
/* Überführt die Serverdaten in die Felder, die die Kartendesigns erwarten.
|
||
Name und Geburtsdatum kommen aus dem Charakter, der Rest aus dem
|
||
jeweiligen Dokument — so bleiben die vorhandenen Designs unverändert. */
|
||
function setzeZustand(z) {
|
||
MAX_SLOTS = z.maxSlots || 10;
|
||
|
||
// Personendaten aus dem Charakter
|
||
person.vorname = z.person.vorname || "";
|
||
person.nachname = z.person.nachname || "";
|
||
person.name = (person.vorname + " " + person.nachname).trim();
|
||
person.geb = z.person.geburt || "—";
|
||
person.geschlecht = z.person.geschlecht || "—";
|
||
|
||
// Dokumentspezifische Felder aus den einzelnen Dokumenten übernehmen
|
||
dokumente = (z.dokumente || []).map(d => {
|
||
const w = d.daten || {};
|
||
if (d.typ === "perso") {
|
||
person.persoNr = w.nummer || "—";
|
||
person.persoGueltig = d.gueltigBis || "—";
|
||
person.gebOrt = w.gebOrt || "Los Santos";
|
||
person.groesse = w.groesse || "—";
|
||
person.nationalitaet = w.nationalitaet || "San Andreas";
|
||
person.adresse = w.adresse || "—";
|
||
person.augen = w.augen || "—";
|
||
} else if (d.typ === "fs") {
|
||
person.fsNr = w.nummer || "—";
|
||
person.fsKlassen = w.klassen || ["B"];
|
||
person.fsGueltig = d.gueltigBis || "—";
|
||
person.fsAusstellung = w.ausstellung || "—";
|
||
} else if (d.typ === "ws") {
|
||
person.wsNr = w.nummer || "—";
|
||
person.wsGueltig = d.gueltigBis || "—";
|
||
person.wsWaffen = w.waffen || [];
|
||
} else if (d.typ === "gk") {
|
||
person.gkVersicherung = w.versicherung || "—";
|
||
person.gkNr = w.nummer || "—";
|
||
person.gkGueltig = d.gueltigBis || "—";
|
||
} else if (d.typ === "vk") {
|
||
person.telefon = w.telefon || "—";
|
||
}
|
||
|
||
// Kurzinfo für die Listenansicht
|
||
let info = d.gueltigBis ? ("gültig bis " + d.gueltigBis) : "";
|
||
if (d.typ === "fs" && w.klassen) info = "Klassen " + w.klassen.join(", ");
|
||
if (d.typ === "gk") info = w.versicherung || info;
|
||
if (d.typ === "vk") info = w.telefon || "keine Nummer";
|
||
|
||
return {
|
||
id: d.typ, // für die Kartendesigns (kartenRenderer)
|
||
dbId: d.id, // für Aktionen am Server
|
||
boerse: d.boerse || 0,
|
||
name: d.name, icon: d.icon, accent: d.accent, info: info,
|
||
};
|
||
});
|
||
|
||
// Börsen aufbauen: so viele, wie der Spieler Börsen-Items besitzt
|
||
const anzahl = Math.max(1, z.boersen || 1);
|
||
boersen = [];
|
||
for (let i = 0; i < anzahl; i++) {
|
||
boersen.push({
|
||
name: "Börse " + (i + 1),
|
||
docs: dokumente.filter(d => d.boerse === i).map(d => d.id),
|
||
});
|
||
}
|
||
|
||
if (aktiveBoerse >= boersen.length) aktiveBoerse = 0;
|
||
if (!akt().docs.includes(aktiv)) aktiv = akt().docs[0] || null;
|
||
|
||
spielerNah = (z.spieler || []).map(sp => ({
|
||
id: sp.id, bekannt: false, name: "Unbekannt", farbe: "#3a3f4d",
|
||
}));
|
||
|
||
render();
|
||
if (z.hinweis) toast(z.hinweis);
|
||
}
|
||
|
||
window.addEventListener("message", (ereignis) => {
|
||
const n = ereignis.data || {};
|
||
|
||
if (n.aktion === "oeffnen") {
|
||
document.body.style.display = "";
|
||
istEigeneBoerseOffen = true;
|
||
setzeZustand(n);
|
||
}
|
||
else if (n.aktion === "zustand") {
|
||
setzeZustand(n);
|
||
}
|
||
else if (n.aktion === "hinweis") {
|
||
toast(n.text || "");
|
||
}
|
||
else if (n.aktion === "fehler") {
|
||
toast(n.text || "Nicht möglich");
|
||
}
|
||
else if (n.aktion === "vorgezeigt") {
|
||
// Jemand zeigt uns ein Dokument — als Vollbild einblenden
|
||
zeigeVorgelegtes(n.dokument);
|
||
}
|
||
else if (n.aktion === "schliessen") {
|
||
istEigeneBoerseOffen = false;
|
||
document.body.style.display = "none";
|
||
schliesseOverlay();
|
||
}
|
||
});
|
||
|
||
// Merkt sich, ob die eigene Börse offen ist (für das Schließen nach dem Vorzeigen)
|
||
let istEigeneBoerseOffen = false;
|
||
|
||
/* Zeigt ein Dokument, das eine ANDERE Person vorlegt.
|
||
Wichtig: Die Kartendesigns lesen aus `person`. Für die Anzeige wird
|
||
daher kurzzeitig auf die Daten des Zeigenden umgeschaltet und danach
|
||
wieder zurück — sonst stünden die eigenen Daten auf fremden Karten. */
|
||
function zeigeVorgelegtes(d) {
|
||
if (!d) return;
|
||
document.body.style.display = "";
|
||
|
||
const eigene = person; // eigene Daten merken
|
||
const w = d.daten || {};
|
||
person = {
|
||
name: d.inhaber || "Unbekannt",
|
||
vorname: (d.inhaber || "").split(" ")[0] || "",
|
||
nachname: (d.inhaber || "").split(" ").slice(1).join(" "),
|
||
geb: d.geburt || "—",
|
||
geschlecht: "—",
|
||
gebOrt: w.gebOrt || "Los Santos", nationalitaet: w.nationalitaet || "San Andreas",
|
||
groesse: w.groesse || "—", augen: w.augen || "—", adresse: w.adresse || "—",
|
||
persoNr: w.nummer || "—", persoGueltig: d.gueltigBis || "—",
|
||
fsNr: w.nummer || "—", fsKlassen: w.klassen || ["B"],
|
||
fsAusstellung: w.ausstellung || "—", fsGueltig: d.gueltigBis || "—",
|
||
wsNr: w.nummer || "—", wsGueltig: d.gueltigBis || "—", wsWaffen: w.waffen || [],
|
||
gkVersicherung: w.versicherung || "—", gkNr: w.nummer || "—", gkGueltig: d.gueltigBis || "—",
|
||
telefon: w.telefon || "—",
|
||
};
|
||
|
||
const karte = renderKarte(d.typ); // mit fremden Daten rendern
|
||
person = eigene; // sofort zurückschalten
|
||
|
||
$("overlay").innerHTML =
|
||
'<div class="present-caption"><div class="pc-h">' + sicher(d.inhaber || "Unbekannt") + ' zeigt: ' + sicher(d.name) + '</div>' +
|
||
'<div class="pc-s">Geboren am ' + sicher(d.geburt || "—") + '</div></div>' +
|
||
'<div class="present-card">' + karte + '</div>' +
|
||
'<button class="btn ghost" id="present-close">Schließen</button>';
|
||
$("overlay").classList.add("show");
|
||
$("present-close").addEventListener("click", () => {
|
||
schliesseOverlay();
|
||
if (!istEigeneBoerseOffen) document.body.style.display = "none";
|
||
});
|
||
}
|
||
|
||
/* ESC schließt die Börse */
|
||
document.addEventListener("keydown", (e) => {
|
||
if (e.key === "Escape" && IST_FIVEM) nui("schliessen", {});
|
||
});
|
||
|
||
/* =====================================================================
|
||
START
|
||
===================================================================== */
|
||
if (IST_FIVEM) {
|
||
document.body.style.display = "none"; // wartet auf "oeffnen"
|
||
} else {
|
||
render(); // Browser-Ansicht mit den Beispieldaten oben
|
||
}
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|