big update
This commit is contained in:
@@ -68,32 +68,6 @@ if (!function_exists('get_version_badge_markup')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined('APP_VERSION_BADGE_REGISTERED')) {
|
||||
register_shutdown_function(function () {
|
||||
if (php_sapi_name() === 'cli') {
|
||||
return;
|
||||
}
|
||||
$headers = headers_list();
|
||||
$isHtmlResponse = true;
|
||||
if (!headers_sent() && !empty($headers)) {
|
||||
$isHtmlResponse = false;
|
||||
foreach ($headers as $header) {
|
||||
$parts = explode(':', $header, 2);
|
||||
$name = strtolower(trim($parts[0] ?? ''));
|
||||
$value = strtolower(trim($parts[1] ?? ''));
|
||||
if ($name === 'content-type' && strpos($value, 'text/html') !== false) {
|
||||
$isHtmlResponse = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($isHtmlResponse) {
|
||||
echo get_version_badge_markup();
|
||||
}
|
||||
});
|
||||
define('APP_VERSION_BADGE_REGISTERED', true);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// set cookie / session parameters
|
||||
// -----------------------------------------------------------
|
||||
|
||||
@@ -1,66 +1,20 @@
|
||||
<?php
|
||||
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||
$appBaseUrl = rtrim($GLOBALS['app_base_url'] ?? '', '/');
|
||||
$assetBase = $appBaseUrl !== '' ? $appBaseUrl : '';
|
||||
$appApiBase = rtrim($GLOBALS['app_api_base'] ?? '', '/');
|
||||
$debugRedirect = isset($_GET['debug_redirect']);
|
||||
$pageTitle = 'Email Template System – Bridge Setup';
|
||||
$pageId = 'bridge-setup';
|
||||
$navActive = 'bridge';
|
||||
$layoutExtraHead = <<<HTML
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
|
||||
.section-card h4{margin:0 0 1rem;font-size:1rem;font-weight:600;color:#0f172a}
|
||||
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
|
||||
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
|
||||
.chip{display:inline-flex;align-items:center;padding:.15rem .55rem;border-radius:999px;background:#f1f5f9;color:#0f172a;border:1px solid #e2e8f0;font-size:.8rem}
|
||||
</style>
|
||||
HTML;
|
||||
require dirname(__DIR__) . '/../structure/layout_start.php';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Email Template System – Bridge Setup</title>
|
||||
<script>document.documentElement.classList.add('auth-pending');</script>
|
||||
<style>html.auth-pending body{visibility:hidden;}</style>
|
||||
<script>
|
||||
window.APP_BASE_URL = <?= json_encode($appBaseUrl, JSON_UNESCAPED_SLASHES) ?>;
|
||||
window.APP_API_BASE = <?= json_encode($appApiBase, JSON_UNESCAPED_SLASHES) ?>;
|
||||
<?php if ($debugRedirect): ?>
|
||||
window.DISABLE_AUTH_REDIRECT = true;
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<?php if ($debugRedirect): ?>
|
||||
<script src="<?= $assetBase ?>/assets/js/debug-location.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
<?php endif; ?>
|
||||
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/admin.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/toast.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.35rem .7rem;border-radius:.7rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}
|
||||
.btn:hover{background:#f8fafc}
|
||||
.btn-avatar{padding:.35rem;border-radius:999px;width:42px;height:42px;justify-content:center;font-weight:600;background:#0ea5e9;color:#fff;border:none}
|
||||
.btn-avatar:hover{background:#0284c7}
|
||||
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
|
||||
.section-card h4{margin:0 0 1rem;font-size:1rem;font-weight:600;color:#0f172a}
|
||||
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
|
||||
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
|
||||
.chip{display:inline-flex;align-items:center;padding:.15rem .55rem;border-radius:999px;background:#f1f5f9;color:#0f172a;border:1px solid #e2e8f0;font-size:.8rem}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800" data-page="bridge-setup">
|
||||
<header class="sticky top-0 z-30 bg-white/90 border-b backdrop-blur">
|
||||
<div class="max-w-4xl mx-auto px-4 py-4 flex items-center gap-3">
|
||||
<a href="<?= $appBaseUrl ?>/admin/settings.php" class="btn" title="Zurück zur Administration">← Administration</a>
|
||||
<h1 class="font-semibold text-lg">Bridge Setup</h1>
|
||||
<div class="ms-auto flex gap-2 items-center">
|
||||
<div class="relative" id="userMenu">
|
||||
<button id="btn-user" type="button" class="btn-avatar" aria-haspopup="true" aria-expanded="false">
|
||||
<span id="userAvatar">U</span>
|
||||
</button>
|
||||
<div id="userMenuPanel" class="user-menu hidden" role="menu">
|
||||
<a href="<?= $appBaseUrl ?>/admin/profile.php" class="user-menu-item" data-menu="profile">Profil</a>
|
||||
<a href="<?= $appBaseUrl ?>/admin/dashboard.php" class="user-menu-item" data-role="admin">Dashboard</a>
|
||||
<a href="<?= $appBaseUrl ?>/admin/settings.php" class="user-menu-item" data-role="admin">Administration</a>
|
||||
<button id="btn-logout" type="button" class="user-menu-item text-red-600">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-4xl mx-auto p-4 md:p-6 space-y-6">
|
||||
<main class="max-w-4xl mx-auto p-4 md:p-6 flex-1 w-full space-y-6">
|
||||
<section class="section-card">
|
||||
<h4>Bridge-Datei vorbereiten</h4>
|
||||
<p class="text-sm text-slate-600 mb-3">
|
||||
@@ -154,13 +108,13 @@ $debugRedirect = isset($_GET['debug_redirect']);
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="toast-root"></div>
|
||||
<div id="toast-root"></div>
|
||||
|
||||
<dialog id="configExampleDialog" class="rounded-xl max-w-2xl w-[90vw]">
|
||||
<form method="dialog" class="space-y-3">
|
||||
<h3 class="text-lg font-semibold">Beispiel: Mapping einer Config-Datei</h3>
|
||||
<p class="text-sm text-slate-600">Angenommen, deine <code>../config/database.php</code> liefert folgendes Array:</p>
|
||||
<pre class="bg-slate-900 text-slate-100 text-xs p-3 rounded-lg overflow-auto"><?php echo htmlspecialchars(<<<'PHP'
|
||||
<dialog id="configExampleDialog" class="rounded-xl max-w-2xl w-[90vw]">
|
||||
<form method="dialog" class="space-y-3">
|
||||
<h3 class="text-lg font-semibold">Beispiel: Mapping einer Config-Datei</h3>
|
||||
<p class="text-sm text-slate-600">Angenommen, deine <code>../config/database.php</code> liefert folgendes Array:</p>
|
||||
<pre class="bg-slate-900 text-slate-100 text-xs p-3 rounded-lg overflow-auto"><?php echo htmlspecialchars(<<<'PHP'
|
||||
<?php
|
||||
return [
|
||||
'database' => [
|
||||
@@ -177,20 +131,21 @@ return [
|
||||
],
|
||||
];
|
||||
PHP, ENT_QUOTES); ?></pre>
|
||||
<p class="text-sm text-slate-600">Dann trägst du ein:</p>
|
||||
<ul class="text-sm text-slate-700 list-disc ps-5">
|
||||
<li><strong>Pfad zur Konfigurationsdatei:</strong> <code>../config/database.php</code></li>
|
||||
<li><strong>Basis-Pfad:</strong> <code>database.connections.default</code></li>
|
||||
<li><strong>Host-/Port-/DB-/User-/Pass-/Charset-Key:</strong> jeweils <code>host</code>, <code>port</code>, <code>database</code>, <code>username</code>, <code>password</code>, <code>charset</code></li>
|
||||
</ul>
|
||||
<p class="text-sm text-slate-600">Die Bridge liest dann automatisch die Werte aus diesem Array und baut daraus den DSN.</p>
|
||||
<div class="text-right">
|
||||
<button type="submit" class="btn">Verstanden</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="<?= $assetBase ?>/assets/js/toast.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
<script type="module" src="<?= $assetBase ?>/assets/js/bridge-setup.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
<p class="text-sm text-slate-600">Dann trägst du ein:</p>
|
||||
<ul class="text-sm text-slate-700 list-disc ps-5">
|
||||
<li><strong>Pfad zur Konfigurationsdatei:</strong> <code>../config/database.php</code></li>
|
||||
<li><strong>Basis-Pfad:</strong> <code>database.connections.default</code></li>
|
||||
<li><strong>Host-/Port-/DB-/User-/Pass-/Charset-Key:</strong> jeweils <code>host</code>, <code>port</code>, <code>database</code>, <code>username</code>, <code>password</code>, <code>charset</code></li>
|
||||
</ul>
|
||||
<p class="text-sm text-slate-600">Die Bridge liest dann automatisch die Werte aus diesem Array und baut daraus den DSN.</p>
|
||||
<div class="text-right">
|
||||
<button type="submit" class="btn">Verstanden</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
<?php
|
||||
$layoutScripts = [
|
||||
['src' => app_asset_url('/assets/js/toast.js')],
|
||||
['src' => app_asset_url('/assets/js/bridge-setup.js'), 'module' => true],
|
||||
];
|
||||
require dirname(__DIR__) . '/../structure/layout_end.php';
|
||||
|
||||
@@ -1,72 +1,22 @@
|
||||
<?php
|
||||
|
||||
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||
$appBaseUrl = rtrim($GLOBALS['app_base_url'] ?? '', '/');
|
||||
$assetBase = $appBaseUrl !== '' ? $appBaseUrl : '';
|
||||
$appApiBase = rtrim($GLOBALS['app_api_base'] ?? '', '/');
|
||||
$debugRedirect = isset($_GET['debug_redirect']);
|
||||
$pageTitle = 'Email Template System – Dashboard';
|
||||
$pageId = 'dashboard';
|
||||
$navActive = 'dashboard';
|
||||
$layoutExtraHead = <<<HTML
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:1.5rem}
|
||||
.stat-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem}
|
||||
.stat-card h4{margin:0;font-size:.95rem;color:#475569}
|
||||
.stat-card strong{display:block;font-size:1.75rem;color:#0f172a}
|
||||
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
|
||||
.usage-table{width:100%;border-collapse:collapse;font-size:.9rem}
|
||||
.usage-table th,.usage-table td{padding:.5rem;border-bottom:1px solid #e2e8f0;text-align:left}
|
||||
</style>
|
||||
HTML;
|
||||
require dirname(__DIR__) . '/../structure/layout_start.php';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Email Template System – Dashboard</title>
|
||||
<script>document.documentElement.classList.add('auth-pending');</script>
|
||||
<style>html.auth-pending body{visibility:hidden;}</style>
|
||||
<script>
|
||||
window.APP_BASE_URL = <?= json_encode($appBaseUrl, JSON_UNESCAPED_SLASHES) ?>;
|
||||
window.APP_API_BASE = <?= json_encode($appApiBase, JSON_UNESCAPED_SLASHES) ?>;
|
||||
<?php if ($debugRedirect): ?>
|
||||
window.DISABLE_AUTH_REDIRECT = true;
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<?php if ($debugRedirect): ?>
|
||||
<script src="<?= $assetBase ?>/assets/js/debug-location.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
<?php endif; ?>
|
||||
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/admin.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/toast.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .8rem;border-radius:.8rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}
|
||||
.btn:hover{background:#f8fafc}
|
||||
.btn-avatar{padding:.35rem;border-radius:999px;width:42px;height:42px;justify-content:center;font-weight:600;background:#0ea5e9;color:#fff;border:none}
|
||||
.btn-avatar:hover{background:#0284c7}
|
||||
.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:1.5rem}
|
||||
.stat-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem}
|
||||
.stat-card h4{margin:0;font-size:.95rem;color:#475569}
|
||||
.stat-card strong{display:block;font-size:1.75rem;color:#0f172a}
|
||||
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
|
||||
.usage-table{width:100%;border-collapse:collapse;font-size:.9rem}
|
||||
.usage-table th,.usage-table td{padding:.5rem;border-bottom:1px solid #e2e8f0;text-align:left}
|
||||
.user-menu{position:absolute;top:calc(100% + .5rem);right:0;min-width:180px;background:#fff;border:1px solid #e2e8f0;border-radius:.75rem;box-shadow:0 20px 35px rgba(15,23,42,.15);padding:.35rem;z-index:40}
|
||||
.user-menu-item{display:block;width:100%;text-align:left;padding:.45rem .75rem;border-radius:.6rem;font-size:.9rem;color:#0f172a}
|
||||
.user-menu-item:hover{background:#f1f5f9}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800" data-page="dashboard">
|
||||
<header class="sticky top-0 z-30 bg-white/90 border-b backdrop-blur">
|
||||
<div class="max-w-5xl mx-auto px-4 py-4 flex items-center gap-3">
|
||||
<a href="<?= $appBaseUrl ?>/index.php" class="btn" title="Zurück zur Übersicht">← Übersicht</a>
|
||||
<h1 class="font-semibold text-lg">Dashboard</h1>
|
||||
<div class="ms-auto flex gap-2 items-center">
|
||||
<div class="relative" id="userMenu">
|
||||
<button id="btn-user" type="button" class="btn-avatar" aria-haspopup="true" aria-expanded="false">
|
||||
<span id="userAvatar">U</span>
|
||||
</button>
|
||||
<div id="userMenuPanel" class="user-menu hidden" role="menu">
|
||||
<a href="<?= $appBaseUrl ?>/admin/profile.php" class="user-menu-item" data-menu="profile">Profil</a>
|
||||
<a href="<?= $appBaseUrl ?>/admin/dashboard.php" class="user-menu-item" data-role="admin">Dashboard</a>
|
||||
<a href="<?= $appBaseUrl ?>/admin/settings.php" class="user-menu-item" data-role="admin">Administration</a>
|
||||
<button id="btn-logout" type="button" class="user-menu-item text-red-600">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-5xl mx-auto p-4 md:p-6">
|
||||
<main class="max-w-5xl mx-auto p-4 md:p-6 flex-1 w-full">
|
||||
<section class="stat-grid" id="dashboardCounts">
|
||||
<div class="stat-card">
|
||||
<h4>Templates</h4>
|
||||
@@ -84,41 +34,41 @@ $debugRedirect = isset($_GET['debug_redirect']);
|
||||
<h4>Snippets</h4>
|
||||
<strong id="count-snippets">–</strong>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Aufrufe gesamt</h4>
|
||||
<strong id="count-usage">–</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-card" data-role="admin">
|
||||
<section class="section-card">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h4>Template-Nutzung</h4>
|
||||
<p class="text-sm text-slate-600">Wie oft wurden Templates über die API geladen? Setze einzelne Zähler bei Bedarf zurück.</p>
|
||||
</div>
|
||||
<button type="button" class="btn" id="btn-refresh-dashboard">Aktualisieren</button>
|
||||
<h3 class="font-semibold text-base text-slate-900">Aktivität</h3>
|
||||
<button type="button" class="btn" id="btn-refresh">Aktualisieren</button>
|
||||
</div>
|
||||
<ul id="activityList" class="space-y-2 text-sm text-slate-600"></ul>
|
||||
</section>
|
||||
|
||||
<section class="section-card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-semibold text-base text-slate-900">Nutzung & Speicher</h3>
|
||||
<span class="text-xs text-slate-500">Letzte 30 Tage</span>
|
||||
</div>
|
||||
<div class="overflow-auto">
|
||||
<table class="usage-table" id="usageTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Template</th>
|
||||
<th>Aufrufe</th>
|
||||
<th>Zuletzt verwendet</th>
|
||||
<th class="text-right">Aktionen</th>
|
||||
<th>Datum</th>
|
||||
<th>Aktionen</th>
|
||||
<th>Templates veröffentlicht</th>
|
||||
<th>Versandtests</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="4" class="text-sm text-slate-500">Lade Daten…</td></tr>
|
||||
</tbody>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="toast-root"></div>
|
||||
|
||||
<script src="<?= $assetBase ?>/assets/js/toast.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
<script type="module" src="<?= $assetBase ?>/assets/js/dashboard.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
$layoutScripts = [
|
||||
['src' => app_asset_url('/assets/js/toast.js')],
|
||||
['src' => app_asset_url('/assets/js/dashboard.js'), 'module' => true],
|
||||
];
|
||||
require dirname(__DIR__) . '/../structure/layout_end.php';
|
||||
|
||||
@@ -1,73 +1,19 @@
|
||||
<?php
|
||||
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||
$appBaseUrl = rtrim($GLOBALS['app_base_url'] ?? '', '/');
|
||||
$assetBase = $appBaseUrl !== '' ? $appBaseUrl : '';
|
||||
$appApiBase = rtrim($GLOBALS['app_api_base'] ?? '', '/');
|
||||
$debugRedirect = isset($_GET['debug_redirect']);
|
||||
$pageTitle = 'Email Template System – Mein Konto';
|
||||
$pageId = 'account';
|
||||
$navActive = 'profile';
|
||||
$layoutExtraHead = <<<HTML
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
|
||||
.section-card h4{margin:0 0 1rem;font-size:1rem;font-weight:600;color:#0f172a}
|
||||
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
|
||||
.user-tabs{display:flex;gap:.5rem;margin-bottom:1.25rem}
|
||||
</style>
|
||||
HTML;
|
||||
require dirname(__DIR__) . '/../structure/layout_start.php';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Email Template System – Konto</title>
|
||||
<script>document.documentElement.classList.add('auth-pending');</script>
|
||||
<style>html.auth-pending body{visibility:hidden;}</style>
|
||||
<script>
|
||||
window.APP_BASE_URL = <?= json_encode($appBaseUrl, JSON_UNESCAPED_SLASHES) ?>;
|
||||
window.APP_API_BASE = <?= json_encode($appApiBase, JSON_UNESCAPED_SLASHES) ?>;
|
||||
<?php if ($debugRedirect): ?>
|
||||
window.DISABLE_AUTH_REDIRECT = true;
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<?php if ($debugRedirect): ?>
|
||||
<script src="<?= $assetBase ?>/assets/js/debug-location.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
<?php endif; ?>
|
||||
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/admin.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/toast.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.35rem .7rem;border-radius:.7rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}
|
||||
.btn:hover{background:#f8fafc}
|
||||
.btn-avatar{padding:.35rem;border-radius:999px;width:42px;height:42px;justify-content:center;font-weight:600;background:#0ea5e9;color:#fff;border:none}
|
||||
.btn-avatar[aria-disabled="true"]{pointer-events:none;}
|
||||
.btn-avatar:hover{background:#0284c7}
|
||||
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
|
||||
.section-card h4{margin:0 0 1rem;font-size:1rem;font-weight:600;color:#0f172a}
|
||||
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
|
||||
.user-tabs{display:flex;gap:.5rem;margin-bottom:1.25rem}
|
||||
.team-table{width:100%;border-collapse:collapse;font-size:.9rem}
|
||||
.team-table th,.team-table td{padding:.35rem .5rem;border-bottom:1px solid #e2e8f0;text-align:left}
|
||||
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
|
||||
.chip{display:inline-flex;align-items:center;padding:.15rem .55rem;border-radius:999px;background:#f1f5f9;color:#0f172a;border:1px solid #e2e8f0;font-size:.8rem}
|
||||
.user-menu{position:absolute;top:calc(100% + .5rem);right:0;min-width:180px;background:#fff;border:1px solid #e2e8f0;border-radius:.75rem;box-shadow:0 20px 35px rgba(15,23,42,.15);padding:.35rem;z-index:40}
|
||||
.user-menu-item{display:block;width:100%;text-align:left;padding:.45rem .75rem;border-radius:.6rem;font-size:.9rem;color:#0f172a}
|
||||
.user-menu-item:hover{background:#f1f5f9}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800" data-page="account">
|
||||
<header class="sticky top-0 z-30 bg-white/90 border-b backdrop-blur">
|
||||
<div class="max-w-5xl mx-auto px-4 py-4 flex items-center gap-3">
|
||||
<a href="<?= $appBaseUrl ?>/index.php" class="btn" title="Zurück zur Übersicht">← Übersicht</a>
|
||||
<h1 class="font-semibold text-lg">Mein Konto</h1>
|
||||
<div class="ms-auto flex gap-2 items-center">
|
||||
<div class="relative" id="userMenu">
|
||||
<button id="btn-user" type="button" class="btn-avatar" aria-haspopup="true" aria-expanded="false">
|
||||
<span id="userAvatar">U</span>
|
||||
</button>
|
||||
<div id="userMenuPanel" class="user-menu hidden" role="menu">
|
||||
<a href="<?= $appBaseUrl ?>/admin/profile.php" class="user-menu-item" data-menu="profile">Profil</a>
|
||||
<a href="<?= $appBaseUrl ?>/admin/dashboard.php" class="user-menu-item" data-role="admin">Dashboard</a>
|
||||
<a href="<?= $appBaseUrl ?>/admin/settings.php" class="user-menu-item" data-role="admin">Administration</a>
|
||||
<button id="btn-logout" type="button" class="user-menu-item text-red-600">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-5xl mx-auto p-4 md:p-6">
|
||||
<main class="max-w-5xl mx-auto p-4 md:p-6 flex-1 w-full">
|
||||
<div class="user-tabs">
|
||||
<button type="button" data-user-tab="profile" class="btn bg-sky-50 text-sky-700 flex-1">Profil</button>
|
||||
<button type="button" data-user-tab="security" class="btn flex-1">Passwort</button>
|
||||
@@ -109,8 +55,9 @@ $debugRedirect = isset($_GET['debug_redirect']);
|
||||
</main>
|
||||
|
||||
<div id="toast-root"></div>
|
||||
|
||||
<script src="<?= $assetBase ?>/assets/js/toast.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
<script type="module" src="<?= $assetBase ?>/assets/js/account.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
$layoutScripts = [
|
||||
['src' => app_asset_url('/assets/js/toast.js')],
|
||||
['src' => app_asset_url('/assets/js/account.js'), 'module' => true],
|
||||
];
|
||||
require dirname(__DIR__) . '/../structure/layout_end.php';
|
||||
|
||||
@@ -1,71 +1,22 @@
|
||||
<?php
|
||||
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||
$appBaseUrl = rtrim($GLOBALS['app_base_url'] ?? '', '/');
|
||||
$assetBase = $appBaseUrl !== '' ? $appBaseUrl : '';
|
||||
$appApiBase = rtrim($GLOBALS['app_api_base'] ?? '', '/');
|
||||
$debugRedirect = isset($_GET['debug_redirect']);
|
||||
$pageTitle = 'Email Template System – Administration';
|
||||
$pageId = 'admin';
|
||||
$navActive = 'settings';
|
||||
$layoutExtraHead = <<<HTML
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
|
||||
.section-card h4{margin:0 0 1rem;font-size:1rem;font-weight:600;color:#0f172a}
|
||||
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
|
||||
.team-table{width:100%;border-collapse:collapse;font-size:.9rem}
|
||||
.team-table th,.team-table td{padding:.35rem .5rem;border-bottom:1px solid #e2e8f0;text-align:left}
|
||||
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
|
||||
.chip{display:inline-flex;align-items:center;padding:.15rem .55rem;border-radius:999px;background:#f1f5f9;color:#0f172a;border:1px solid #e2e8f0;font-size:.8rem}
|
||||
</style>
|
||||
HTML;
|
||||
require dirname(__DIR__) . '/../structure/layout_start.php';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Email Template System – Administration</title>
|
||||
<script>document.documentElement.classList.add('auth-pending');</script>
|
||||
<style>html.auth-pending body{visibility:hidden;}</style>
|
||||
<script>
|
||||
window.APP_BASE_URL = <?= json_encode($appBaseUrl, JSON_UNESCAPED_SLASHES) ?>;
|
||||
window.APP_API_BASE = <?= json_encode($appApiBase, JSON_UNESCAPED_SLASHES) ?>;
|
||||
<?php if ($debugRedirect): ?>
|
||||
window.DISABLE_AUTH_REDIRECT = true;
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<?php if ($debugRedirect): ?>
|
||||
<script src="<?= $assetBase ?>/assets/js/debug-location.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
<?php endif; ?>
|
||||
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/admin.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/toast.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.35rem .7rem;border-radius:.7rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}
|
||||
.btn:hover{background:#f8fafc}
|
||||
.btn-avatar{padding:.35rem;border-radius:999px;width:42px;height:42px;justify-content:center;font-weight:600;background:#0ea5e9;color:#fff;border:none}
|
||||
.btn-avatar:hover{background:#0284c7}
|
||||
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
|
||||
.section-card h4{margin:0 0 1rem;font-size:1rem;font-weight:600;color:#0f172a}
|
||||
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
|
||||
.team-table{width:100%;border-collapse:collapse;font-size:.9rem}
|
||||
.team-table th,.team-table td{padding:.35rem .5rem;border-bottom:1px solid #e2e8f0;text-align:left}
|
||||
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
|
||||
.chip{display:inline-flex;align-items:center;padding:.15rem .55rem;border-radius:999px;background:#f1f5f9;color:#0f172a;border:1px solid #e2e8f0;font-size:.8rem}
|
||||
.user-menu{position:absolute;top:calc(100% + .5rem);right:0;min-width:180px;background:#fff;border:1px solid #e2e8f0;border-radius:.75rem;box-shadow:0 20px 35px rgba(15,23,42,.15);padding:.35rem;z-index:40}
|
||||
.user-menu-item{display:block;width:100%;text-align:left;padding:.45rem .75rem;border-radius:.6rem;font-size:.9rem;color:#0f172a}
|
||||
.user-menu-item:hover{background:#f1f5f9}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800" data-page="admin">
|
||||
<header class="sticky top-0 z-30 bg-white/90 border-b backdrop-blur">
|
||||
<div class="max-w-5xl mx-auto px-4 py-4 flex items-center gap-3">
|
||||
<a href="<?= $appBaseUrl ?>/index.php" class="btn" title="Zurück zur Übersicht">← Übersicht</a>
|
||||
<h1 class="font-semibold text-lg">Administration</h1>
|
||||
<div class="ms-auto flex gap-2 items-center">
|
||||
<div class="relative" id="userMenu">
|
||||
<button id="btn-user" type="button" class="btn-avatar" aria-haspopup="true" aria-expanded="false">
|
||||
<span id="userAvatar">U</span>
|
||||
</button>
|
||||
<div id="userMenuPanel" class="user-menu hidden" role="menu">
|
||||
<a href="<?= $appBaseUrl ?>/admin/profile.php" class="user-menu-item" data-menu="profile">Profil</a>
|
||||
<a href="<?= $appBaseUrl ?>/admin/dashboard.php" class="user-menu-item" data-role="admin">Dashboard</a>
|
||||
<a href="<?= $appBaseUrl ?>/admin/settings.php" class="user-menu-item" data-role="admin">Administration</a>
|
||||
<button id="btn-logout" type="button" class="user-menu-item text-red-600">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-5xl mx-auto p-4 md:p-6 space-y-6">
|
||||
<main class="max-w-5xl mx-auto p-4 md:p-6 flex-1 w-full space-y-6">
|
||||
<section class="section-card" data-role="owner">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4>Team</h4>
|
||||
@@ -171,7 +122,7 @@ $debugRedirect = isset($_GET['debug_redirect']);
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<a href="<?= $appBaseUrl ?>/admin/bridge.php" class="btn w-max" data-role="admin">Bridge-Setup & Tabellen öffnen</a>
|
||||
<a href="<?= htmlspecialchars($appBaseUrl . '/admin/bridge.php') ?>" class="btn w-max" data-role="admin">Bridge-Setup & Tabellen öffnen</a>
|
||||
<p class="text-xs text-slate-500">Dort kannst du Tabellen-Filter sowie DB-Quellen für die Bridge-Datei konfigurieren.</p>
|
||||
</div>
|
||||
<div class="flex justify-between gap-2 flex-wrap pt-2">
|
||||
@@ -186,8 +137,9 @@ $debugRedirect = isset($_GET['debug_redirect']);
|
||||
</main>
|
||||
|
||||
<div id="toast-root"></div>
|
||||
|
||||
<script src="<?= $assetBase ?>/assets/js/toast.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
<script type="module" src="<?= $assetBase ?>/assets/js/account.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
$layoutScripts = [
|
||||
['src' => app_asset_url('/assets/js/toast.js')],
|
||||
['src' => app_asset_url('/assets/js/account.js'), 'module' => true],
|
||||
];
|
||||
require dirname(__DIR__) . '/../structure/layout_end.php';
|
||||
|
||||
40
partials/structure/app_config.php
Normal file
40
partials/structure/app_config.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
// partials/structure/app_config.php
|
||||
// Gemeinsame Basisparameter für die Landingpages im Emailtemplate-Projekt.
|
||||
|
||||
if (!isset($layoutContext) || !is_array($layoutContext)) {
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
|
||||
$layoutContext = [
|
||||
'app_base_url' => rtrim($GLOBALS['app_base_url'] ?? ($scheme . '://' . $host), '/'),
|
||||
'app_api_base' => rtrim($GLOBALS['app_api_base'] ?? ($scheme . '://' . $host . '/api'), '/'),
|
||||
'asset_version' => defined('ASSET_VERSION') ? ASSET_VERSION : time(),
|
||||
'app_env' => $GLOBALS['app_env'] ?? (defined('APP_ENV') ? APP_ENV : 'prod'),
|
||||
];
|
||||
|
||||
$layoutContext['asset_base'] = $layoutContext['app_base_url'] ?: '';
|
||||
$layoutContext['debug_redirect'] = isset($_GET['debug_redirect']);
|
||||
$GLOBALS['layoutContext'] = $layoutContext;
|
||||
}
|
||||
|
||||
$GLOBALS['layoutContext'] = $layoutContext;
|
||||
|
||||
$appBaseUrl = $layoutContext['app_base_url'];
|
||||
$appApiBase = $layoutContext['app_api_base'];
|
||||
$assetBase = $layoutContext['asset_base'];
|
||||
$assetVersion = $layoutContext['asset_version'];
|
||||
$debugRedirect = $layoutContext['debug_redirect'];
|
||||
|
||||
if (!function_exists('app_asset_url')) {
|
||||
function app_asset_url(string $path, ?int $version = null): string
|
||||
{
|
||||
$base = $GLOBALS['layoutContext']['asset_base'] ?? '';
|
||||
$version = $version ?? ($GLOBALS['layoutContext']['asset_version'] ?? null);
|
||||
$url = rtrim($base, '/') . $path;
|
||||
if ($version !== null) {
|
||||
$url .= (strpos($url, '?') === false ? '?' : '&') . 'v=' . rawurlencode((string)$version);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
15
partials/structure/footer.php
Normal file
15
partials/structure/footer.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
$version = $GLOBALS['app_version'] ?? null;
|
||||
?>
|
||||
<footer class="app-footer border-t border-slate-200 bg-white mt-8">
|
||||
<div class="max-w-6xl mx-auto px-4 py-6 flex flex-wrap items-center gap-3 text-sm text-slate-500">
|
||||
<span>© <?= date('Y') ?> Email Template System</span>
|
||||
<span class="ms-auto inline-flex items-center gap-1 text-slate-600">
|
||||
<span class="text-xs uppercase tracking-[0.2em] text-slate-400">Version</span>
|
||||
<strong><?= htmlspecialchars($version ?? '—') ?></strong>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
<?php if ($version): ?>
|
||||
<div class="app-version-fixed">v <?= htmlspecialchars($version) ?></div>
|
||||
<?php endif; ?>
|
||||
47
partials/structure/header.php
Normal file
47
partials/structure/header.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/** @var array $layoutContext */
|
||||
$appBaseUrl = $layoutContext['app_base_url'] ?? '';
|
||||
|
||||
$navLinks = $navLinks ?? [
|
||||
['id' => 'dashboard', 'label' => 'Dashboard', 'href' => $appBaseUrl . '/admin/dashboard.php'],
|
||||
['id' => 'settings', 'label' => 'Administration','href' => $appBaseUrl . '/admin/settings.php'],
|
||||
['id' => 'bridge', 'label' => 'Bridge Setup', 'href' => $appBaseUrl . '/admin/bridge.php'],
|
||||
['id' => 'profile', 'label' => 'Mein Konto', 'href' => $appBaseUrl . '/admin/profile.php'],
|
||||
];
|
||||
|
||||
$navActive = $navActive ?? null;
|
||||
?>
|
||||
<header class="site-header sticky top-0 z-40 border-b border-slate-200 bg-white/90 backdrop-blur">
|
||||
<div class="max-w-6xl mx-auto px-4 lg:px-6 flex items-center gap-4 py-3">
|
||||
<a href="<?= htmlspecialchars($appBaseUrl . '/index.php') ?>" class="flex items-center gap-2 text-slate-800 font-semibold">
|
||||
<span class="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-sky-500 text-white font-bold">ET</span>
|
||||
<span class="hidden sm:block">Email Template System</span>
|
||||
</a>
|
||||
|
||||
<nav class="ms-auto hidden md:flex items-center gap-2 text-sm font-medium text-slate-500">
|
||||
<?php foreach ($navLinks as $link): ?>
|
||||
<?php
|
||||
$isActive = $navActive && $navActive === ($link['id'] ?? null);
|
||||
$classes = 'px-3 py-1.5 rounded-full transition-colors';
|
||||
$classes .= $isActive ? ' bg-sky-100 text-sky-700' : ' hover:text-slate-900';
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($link['href']) ?>" class="<?= $classes ?>">
|
||||
<?= htmlspecialchars($link['label']) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
<div class="relative ms-auto md:ms-4" id="userMenu">
|
||||
<button id="btn-user" type="button" class="btn-avatar" aria-haspopup="true" aria-expanded="false">
|
||||
<span id="userAvatar">U</span>
|
||||
</button>
|
||||
<div id="userMenuPanel" class="user-menu hidden" role="menu">
|
||||
<a href="<?= htmlspecialchars($appBaseUrl . '/admin/profile.php') ?>" class="user-menu-item" data-menu="profile">Profil</a>
|
||||
<a href="<?= htmlspecialchars($appBaseUrl . '/admin/dashboard.php') ?>" class="user-menu-item" data-role="admin">Dashboard</a>
|
||||
<a href="<?= htmlspecialchars($appBaseUrl . '/admin/settings.php') ?>" class="user-menu-item" data-role="admin">Administration</a>
|
||||
<a href="<?= htmlspecialchars($appBaseUrl . '/admin/bridge.php') ?>" class="user-menu-item" data-role="admin">Bridge Setup</a>
|
||||
<button id="btn-logout" type="button" class="user-menu-item text-red-600">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
27
partials/structure/layout_end.php
Normal file
27
partials/structure/layout_end.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
$layoutScripts = $layoutScripts ?? [];
|
||||
if (!is_array($layoutScripts)) {
|
||||
$layoutScripts = [$layoutScripts];
|
||||
}
|
||||
?>
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
<?php foreach ($layoutScripts as $script): ?>
|
||||
<?php if (is_string($script)): ?>
|
||||
<?= $script . PHP_EOL ?>
|
||||
<?php elseif (is_array($script) && isset($script['src'])): ?>
|
||||
<?php
|
||||
$attrs = [];
|
||||
if (!empty($script['defer'])) {
|
||||
$attrs[] = 'defer';
|
||||
}
|
||||
if (!empty($script['module'])) {
|
||||
$attrs[] = 'type="module"';
|
||||
} elseif (!empty($script['type'])) {
|
||||
$attrs[] = 'type="' . htmlspecialchars($script['type'], ENT_QUOTES) . '"';
|
||||
}
|
||||
?>
|
||||
<script src="<?= htmlspecialchars($script['src'], ENT_QUOTES) ?>" <?= implode(' ', $attrs) ?>></script>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</body>
|
||||
</html>
|
||||
57
partials/structure/layout_start.php
Normal file
57
partials/structure/layout_start.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/app_config.php';
|
||||
|
||||
$pageTitle = $pageTitle ?? 'Email Template System';
|
||||
$bodyClass = trim(($bodyClass ?? 'bg-slate-50 text-slate-800') . ' page-shell');
|
||||
$pageId = $pageId ?? null;
|
||||
$navActive = $navActive ?? null;
|
||||
$layoutExtraHead = $layoutExtraHead ?? [];
|
||||
if (!is_array($layoutExtraHead)) {
|
||||
$layoutExtraHead = [$layoutExtraHead];
|
||||
}
|
||||
|
||||
$sharedCss = [
|
||||
app_asset_url('/assets/css/admin.css'),
|
||||
app_asset_url('/assets/css/toast.css'),
|
||||
];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||
<script>document.documentElement.classList.add('auth-pending');</script>
|
||||
<style>html.auth-pending body{visibility:hidden;}</style>
|
||||
<script>
|
||||
window.APP_BASE_URL = <?= json_encode($appBaseUrl, JSON_UNESCAPED_SLASHES) ?>;
|
||||
window.APP_API_BASE = <?= json_encode($appApiBase, JSON_UNESCAPED_SLASHES) ?>;
|
||||
<?php if ($debugRedirect): ?>
|
||||
window.DISABLE_AUTH_REDIRECT = true;
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<?php if ($debugRedirect): ?>
|
||||
<script src="<?= app_asset_url('/assets/js/debug-location.js') ?>"></script>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($sharedCss as $href): ?>
|
||||
<link rel="stylesheet" href="<?= htmlspecialchars($href, ENT_QUOTES) ?>">
|
||||
<?php endforeach; ?>
|
||||
<style>
|
||||
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.35rem .7rem;border-radius:.75rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}
|
||||
.btn:hover{background:#f8fafc}
|
||||
.btn-avatar{padding:.35rem;border-radius:999px;width:42px;height:42px;justify-content:center;font-weight:600;background:#0ea5e9;color:#fff;border:none}
|
||||
.btn-avatar:hover{background:#0284c7}
|
||||
.user-menu{position:absolute;top:calc(100% + .5rem);right:0;min-width:200px;background:#fff;border:1px solid #e2e8f0;border-radius:.9rem;box-shadow:0 20px 35px rgba(15,23,42,.15);padding:.4rem;z-index:40}
|
||||
.user-menu-item{display:block;width:100%;text-align:left;padding:.45rem .75rem;border-radius:.65rem;font-size:.9rem;color:#0f172a}
|
||||
.user-menu-item:hover{background:#f1f5f9}
|
||||
.page-shell{min-height:100vh;display:flex;flex-direction:column;}
|
||||
.app-version-fixed{position:fixed;right:12px;bottom:12px;z-index:2147483000;font-size:12px;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#0f172a;background:rgba(248,250,252,.85);border:1px solid rgba(148,163,184,.6);border-radius:999px;padding:4px 10px;box-shadow:0 8px 20px rgba(15,23,42,.15);backdrop-filter:blur(6px);}
|
||||
@media print {.app-version-fixed{display:none}}
|
||||
</style>
|
||||
<?php foreach ($layoutExtraHead as $snippet): ?>
|
||||
<?= $snippet . PHP_EOL ?>
|
||||
<?php endforeach; ?>
|
||||
</head>
|
||||
<body class="<?= htmlspecialchars($bodyClass) ?>" <?= $pageId ? 'data-page="' . htmlspecialchars($pageId) . '"' : '' ?>>
|
||||
<?php require __DIR__ . '/header.php'; ?>
|
||||
39
partials/structure/matomo.php
Normal file
39
partials/structure/matomo.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php if (!defined('MATOMO_SITE_ID')) return; ?>
|
||||
<?php if (!defined('MATOMO_ENABLED') || !MATOMO_ENABLED) return; ?>
|
||||
<?php
|
||||
$matomoDomains = [];
|
||||
$primaryDomain = app_primary_domain();
|
||||
$fakecheckDomain = app_fakecheck_domain();
|
||||
|
||||
$matomoDomains[] = '*.' . $primaryDomain;
|
||||
$matomoDomains[] = '*.' . $fakecheckDomain;
|
||||
$matomoDomains[] = '*.' . $primaryDomain . '/fakecheck';
|
||||
?>
|
||||
|
||||
<!-- Matomo -->
|
||||
<script>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
_paq.push(["setDomains", <?= json_encode($matomoDomains, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>]);
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
|
||||
(function() {
|
||||
var u = "<?= rtrim(MATOMO_URL, '/') ?>/";
|
||||
_paq.push(['setTrackerUrl', u + 'matomo.php']);
|
||||
_paq.push(['setSiteId', '<?= MATOMO_SITE_ID ?>']);
|
||||
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true;
|
||||
g.src=u + 'matomo.js';
|
||||
s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<noscript>
|
||||
<p>
|
||||
<img referrerpolicy="no-referrer-when-downgrade"
|
||||
src="<?= rtrim(MATOMO_URL,'/') ?>/matomo.php?idsite=<?= MATOMO_SITE_ID ?>&rec=1"
|
||||
style="border:0;" alt="" />
|
||||
</p>
|
||||
</noscript>
|
||||
<!-- End Matomo -->
|
||||
320
src/functions.php
Normal file
320
src/functions.php
Normal file
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
$GLOBALS['page_header_scripts'] = $GLOBALS['page_header_scripts'] ?? [];
|
||||
$GLOBALS['page_footer_scripts'] = $GLOBALS['page_footer_scripts'] ?? [];
|
||||
$GLOBALS['page_styles'] = $GLOBALS['page_styles'] ?? [];
|
||||
|
||||
/**
|
||||
* Primäre Domain / URL (Brand)
|
||||
*/
|
||||
function app_primary_domain(): string
|
||||
{
|
||||
return defined('APP_DOMAIN_PRIMARY') ? APP_DOMAIN_PRIMARY : 'usbcheck.it';
|
||||
}
|
||||
|
||||
function app_primary_url(): string
|
||||
{
|
||||
$url = defined('APP_URL_PRIMARY') ? APP_URL_PRIMARY : 'https://usbcheck.it';
|
||||
return rtrim($url, '/');
|
||||
}
|
||||
|
||||
function app_fakecheck_domain(): string
|
||||
{
|
||||
return defined('APP_DOMAIN_FAKECHECK') ? APP_DOMAIN_FAKECHECK : 'ismyusbfake.com';
|
||||
}
|
||||
|
||||
function app_fakecheck_url(): string
|
||||
{
|
||||
$url = defined('APP_URL_FAKECHECK') ? APP_URL_FAKECHECK : 'https://ismyusbfake.com';
|
||||
return rtrim($url, '/');
|
||||
}
|
||||
|
||||
/* -------------------------------------------------
|
||||
Zentrale Request-/URL-Helper
|
||||
------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Aktuelles Schema (http / https), inkl. Proxy-Header-Fallback.
|
||||
*/
|
||||
function app_request_scheme(): string
|
||||
{
|
||||
// Proxy / Loadbalancer
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
|
||||
$proto = strtolower((string)$_SERVER['HTTP_X_FORWARDED_PROTO']);
|
||||
if ($proto === 'https' || $proto === 'http') {
|
||||
return $proto;
|
||||
}
|
||||
}
|
||||
|
||||
// Direkt
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
return 'https';
|
||||
}
|
||||
|
||||
return 'http';
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktueller Host (inkl. Port, falls im Host-Header).
|
||||
*/
|
||||
function app_request_host(): string
|
||||
{
|
||||
return $_SERVER['HTTP_HOST'] ?? app_primary_domain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Basis-URL der aktuellen Anfrage, z. B. https://staging.usbcheck.it
|
||||
*/
|
||||
function app_current_base_url(): string
|
||||
{
|
||||
return app_request_scheme() . '://' . app_request_host();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktueller Pfad ohne Query, z. B. /login/ oder /fakecheck/demo
|
||||
*/
|
||||
function app_current_path(): string
|
||||
{
|
||||
return strtok($_SERVER['REQUEST_URI'] ?? '/', '?');
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktuelle URL, optional mit Query-String.
|
||||
*/
|
||||
function app_current_url(bool $withQuery = true): string
|
||||
{
|
||||
$base = app_current_base_url();
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
|
||||
if ($withQuery) {
|
||||
return $base . $uri;
|
||||
}
|
||||
|
||||
return $base . strtok($uri, '?');
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical-URL bestimmen.
|
||||
* - Wenn $override gesetzt: exakt diesen Wert verwenden.
|
||||
* - Sonst: aktuelle URL ohne Query.
|
||||
*/
|
||||
function app_canonical_url(?string $override = null): string
|
||||
{
|
||||
if (is_string($override) && $override !== '') {
|
||||
return $override;
|
||||
}
|
||||
|
||||
return app_current_url(false);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------
|
||||
Assets: JS / CSS
|
||||
------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Script registrieren
|
||||
*/
|
||||
function tpl_add_script(
|
||||
string $src,
|
||||
string $pos = 'footer',
|
||||
bool $defer = true,
|
||||
bool $async = false,
|
||||
string $type = '',
|
||||
?string $version = null
|
||||
): void {
|
||||
if ($version === null && defined('ASSET_VERSION')) {
|
||||
$version = ASSET_VERSION;
|
||||
}
|
||||
|
||||
$GLOBALS['page_header_scripts'] = $GLOBALS['page_header_scripts'] ?? [];
|
||||
$GLOBALS['page_footer_scripts'] = $GLOBALS['page_footer_scripts'] ?? [];
|
||||
|
||||
$data = [
|
||||
'src' => $src,
|
||||
'defer' => $defer,
|
||||
'async' => $async,
|
||||
'type' => $type,
|
||||
'version' => $version,
|
||||
];
|
||||
|
||||
if ($pos === 'header') {
|
||||
$GLOBALS['page_header_scripts'][] = $data;
|
||||
} else {
|
||||
$GLOBALS['page_footer_scripts'][] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS registrieren – MIT Cache-Buster und Priorität
|
||||
*
|
||||
* @param string $href Pfad zur CSS-Datei oder externe URL
|
||||
* @param string $pos aktuell nur 'header' relevant (aber für Symmetrie drin)
|
||||
* @param string $priority 'early' | 'normal' | 'late'
|
||||
* @param string|null $version null = nutze ASSET_VERSION (falls definiert),
|
||||
* '' = kein ?v=,
|
||||
* 'xyz'= erzwinge diese Version
|
||||
*/
|
||||
function tpl_add_style(
|
||||
string $href,
|
||||
string $pos = 'header',
|
||||
string $priority = 'normal',
|
||||
?string $version = null
|
||||
): void {
|
||||
if ($version === null && defined('ASSET_VERSION')) {
|
||||
$version = ASSET_VERSION;
|
||||
}
|
||||
|
||||
$GLOBALS['page_styles'][] = [
|
||||
'href' => $href,
|
||||
'pos' => $pos,
|
||||
'priority' => $priority,
|
||||
'version' => $version,
|
||||
];
|
||||
}
|
||||
|
||||
/* -------------------------------------------------
|
||||
Template Loader
|
||||
------------------------------------------------- */
|
||||
|
||||
function tpl(string $file, string $type = 'structure', string $site = 'main'): void
|
||||
{
|
||||
$base = __DIR__ . '/../partials/';
|
||||
|
||||
if (preg_match('/[^a-zA-Z0-9_\-]/', $file)) {
|
||||
echo "<!-- tpl(): Ungültiger Template-Name -->";
|
||||
return;
|
||||
}
|
||||
if (preg_match('/[^a-zA-Z0-9_\-]/', $type)) {
|
||||
echo "<!-- tpl(): Ungültiger Type -->";
|
||||
return;
|
||||
}
|
||||
if (preg_match('/[^a-zA-Z0-9_\-]/', $site)) {
|
||||
echo "<!-- tpl(): Ungültiger Site -->";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'landing') {
|
||||
$path = $base . "landing/$site/$file.php";
|
||||
} else {
|
||||
$path = $base . "structure/$file.php";
|
||||
}
|
||||
|
||||
extract($GLOBALS, EXTR_SKIP);
|
||||
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
} else {
|
||||
echo "<!-- tpl(): Datei nicht gefunden: $path -->";
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------
|
||||
Flash-Messages
|
||||
------------------------------------------------- */
|
||||
|
||||
function flash_set(string $type, string $message, ?string $context = null): void
|
||||
{
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
@session_start();
|
||||
}
|
||||
|
||||
$_SESSION['flash'] = [
|
||||
'type' => $type,
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
];
|
||||
}
|
||||
|
||||
function flash_get(): ?array
|
||||
{
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
@session_start();
|
||||
}
|
||||
|
||||
if (empty($_SESSION['flash']) || !is_array($_SESSION['flash'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$flash = $_SESSION['flash'];
|
||||
unset($_SESSION['flash']);
|
||||
|
||||
$flash['type'] = $flash['type'] ?? 'info';
|
||||
$flash['message'] = $flash['message'] ?? '';
|
||||
$flash['context'] = $flash['context'] ?? null;
|
||||
|
||||
return $flash;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------
|
||||
i18n Helper
|
||||
------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* interner Helper für dot-notation keys
|
||||
*/
|
||||
function _i18n_traverse_array(array $data, string $key)
|
||||
{
|
||||
$segments = explode('.', $key);
|
||||
$node = $data;
|
||||
|
||||
foreach ($segments as $seg) {
|
||||
if (!is_array($node) || !array_key_exists($seg, $node)) {
|
||||
return null;
|
||||
}
|
||||
$node = $node[$seg];
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hauptfunktion i18n_get
|
||||
*/
|
||||
function i18n_get(string $path, $default = null, array $replacements = []): string
|
||||
{
|
||||
if (!isset($GLOBALS['i18n']) || !is_array($GLOBALS['i18n'])) {
|
||||
return $default !== null ? (string)$default : '';
|
||||
}
|
||||
|
||||
$current = $GLOBALS['i18n']['current'] ?? [];
|
||||
$fallback = $GLOBALS['i18n']['fallback'] ?? [];
|
||||
|
||||
$value = _i18n_traverse_array($current, $path);
|
||||
|
||||
if ($value === null && !empty($fallback)) {
|
||||
$value = _i18n_traverse_array($fallback, $path);
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
return $default !== null ? (string)$default : '';
|
||||
}
|
||||
|
||||
$text = $value;
|
||||
|
||||
// built-in placeholder
|
||||
$builtIn = [
|
||||
'{year}' => date('Y'),
|
||||
'{{primary_domain}}' => function_exists('app_primary_domain') ? app_primary_domain() : '',
|
||||
'{{primary_url}}' => function_exists('app_primary_url') ? app_primary_url() : '',
|
||||
];
|
||||
|
||||
foreach ($builtIn as $ph => $val) {
|
||||
$text = str_replace($ph, $val, $text);
|
||||
}
|
||||
|
||||
foreach ($replacements as $key => $val) {
|
||||
$val = (string)$val;
|
||||
$text = str_replace('{' . $key . '}', $val, $text);
|
||||
$text = str_replace('{{' . $key . '}}', $val, $text);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kurzform für i18n_get
|
||||
*/
|
||||
function i18n_get_fmt(string $path, $default = '', array $vars = []): string
|
||||
{
|
||||
$def = $default ?? '';
|
||||
return i18n_get($path, $def, $vars);
|
||||
}
|
||||
Reference in New Issue
Block a user