LAN_Portal/index.php
2026-10-16 16:22:59 -04:00

187 lines
9.2 KiB
PHP
Executable file

<?php
const PORTAL = true;
const CONFIG_FILE = 'config.json';
const ACCENTS = ['blue', 'green', 'orange', 'purple'];
const MODES = ['auto', 'light', 'dark'];
function e($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function safe_url($u) { return preg_match('~^(https?://|/(?!/))~i', (string)$u) ? $u : '#'; }
$config = json_decode((string)@file_get_contents(CONFIG_FILE), true);
$configError = null;
if (!is_array($config)) {
$configError = 'config.json is missing or malformed, so defaults are shown.';
$config = ['title' => 'Portal', 'welcome' => '', 'links' => []];
}
// Set by admin.php when it exists; otherwise the portal stays read-only.
$isAdmin = false;
$csrf = '';
$flash = null;
if (is_file(__DIR__ . '/admin.php')) require __DIR__ . '/admin.php';
$mode = in_array($config['appearance']['mode'] ?? '', MODES, true) ? $config['appearance']['mode'] : 'auto';
$accent = in_array($config['appearance']['accent'] ?? '', ACCENTS, true) ? $config['appearance']['accent'] : 'blue';
$edit = ($isAdmin && is_string($_GET['edit'] ?? null)) ? $_GET['edit'] : '';
$links = array_values(array_filter($config['links'] ?? [], 'is_array'));
if ($edit === 'new') $links[] = ['id' => 'new', 'enabled' => true];
?>
<!doctype html>
<html lang="en" data-bs-theme="<?= $mode === 'dark' ? 'dark' : 'light' ?>" data-accent="<?= e($accent) ?>"
data-default-mode="<?= e($mode) ?>" data-default-accent="<?= e($accent) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($config['title'] ?? 'Portal') ?></title>
<link rel="stylesheet" href="bootstrap.min.css">
<link rel="stylesheet" href="portal.css">
<script src="theme.js"></script>
</head>
<body>
<main class="container py-4">
<?php if ($flash): ?><div class="alert alert-<?= e($flash[0]) ?>"><?= e($flash[1]) ?></div><?php endif; ?>
<?php if ($configError): ?><div class="alert alert-danger"><?= e($configError) ?></div><?php endif; ?>
<?php if ($isAdmin): ?>
<div class="alert alert-secondary d-flex flex-wrap gap-2 align-items-center">
<strong class="me-auto">Editing mode</strong>
<a class="btn btn-sm btn-outline-secondary" href="?edit=appearance">✎ Site default theme</a>
<form method="post">
<input type="hidden" name="csrf" value="<?= e($csrf) ?>">
<button class="btn btn-sm btn-outline-secondary" name="action" value="exit">Exit editing</button>
</form>
</div>
<?php if ($edit === 'appearance'): ?>
<form method="post" class="card card-body mb-4">
<input type="hidden" name="csrf" value="<?= e($csrf) ?>">
<div class="row g-2 mb-2">
<div class="col-sm-6"><label class="form-label">Default mode</label>
<select name="mode" class="form-select">
<?php foreach (MODES as $m): ?><option value="<?= $m ?>"<?= $m === $mode ? ' selected' : '' ?>><?= $m === 'auto' ? 'follow device' : $m ?></option><?php endforeach; ?>
</select></div>
<div class="col-sm-6"><label class="form-label">Default accent</label>
<select name="accent" class="form-select">
<?php foreach (ACCENTS as $a): ?><option value="<?= $a ?>"<?= $a === $accent ? ' selected' : '' ?>><?= $a ?></option><?php endforeach; ?>
</select></div>
</div>
<div><button class="btn btn-accent" name="action" value="save_appearance">Save</button>
<a class="btn btn-outline-secondary" href="?">Cancel</a></div>
</form>
<?php endif; ?>
<?php endif; ?>
<?php
// Banner features, shown above the title with no panel around them. Add a path here to put a feature at the top.
foreach (['announcement/announcement.php'] as $part) {
if (is_file(__DIR__ . '/' . $part)) include __DIR__ . '/' . $part;
}
?>
<header class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
<div class="flex-grow-1">
<?php if ($edit === 'site'): ?>
<form method="post">
<input type="hidden" name="csrf" value="<?= e($csrf) ?>">
<label class="form-label">Title</label>
<input name="title" class="form-control mb-2" maxlength="80" required value="<?= e($config['title'] ?? '') ?>">
<label class="form-label">Welcome message</label>
<input name="welcome" class="form-control mb-2" maxlength="300" value="<?= e($config['welcome'] ?? '') ?>">
<button class="btn btn-accent" name="action" value="save_site">Save</button>
<a class="btn btn-outline-secondary" href="?">Cancel</a>
</form>
<?php else: ?>
<h1 class="h2"><?= e($config['title'] ?? '') ?></h1>
<?php if ($isAdmin): ?><a class="edit" href="?edit=site" title="Edit title and welcome" aria-label="Edit title and welcome">✎</a><?php endif; ?>
<p class="lead mb-0"><?= e($config['welcome'] ?? '') ?></p>
<?php endif; ?>
</div>
<div id="appearance" hidden>
<div class="btn-group btn-group-sm" role="group" aria-label="Colour mode">
<button type="button" class="btn btn-outline-secondary" data-mode="light">Light</button>
<button type="button" class="btn btn-outline-secondary" data-mode="dark">Dark</button>
<button type="button" class="btn btn-outline-secondary" data-mode="auto">Device</button>
</div>
<span class="d-inline-flex gap-1 align-middle ms-2">
<?php foreach (ACCENTS as $a): ?><button type="button" class="swatch" data-pick="<?= $a ?>" aria-label="<?= $a ?> accent" title="<?= $a ?>"></button><?php endforeach; ?>
</span>
<button type="button" class="btn btn-sm btn-link" data-reset="1">Reset</button>
</div>
</header>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mb-4">
<?php foreach ($links as $l):
$id = (string)($l['id'] ?? '');
$on = !empty($l['enabled']);
if (!$on && !$isAdmin) continue; ?>
<div class="col"><div class="card h-100<?= $on ? '' : ' disabled-card' ?>">
<div class="card-body">
<?php if ($isAdmin && $edit === $id && $id !== ''): ?>
<form method="post">
<input type="hidden" name="csrf" value="<?= e($csrf) ?>">
<input type="hidden" name="id" value="<?= e($id) ?>">
<input name="title" class="form-control mb-2" placeholder="Title" maxlength="60" required value="<?= e($l['title'] ?? '') ?>">
<input name="description" class="form-control mb-2" placeholder="Description" maxlength="200" value="<?= e($l['description'] ?? '') ?>">
<input name="url" class="form-control mb-2" placeholder="http://192.168.50.1:8080" maxlength="300" required value="<?= e($l['url'] ?? '') ?>">
<input name="icon" class="form-control mb-2" placeholder="Icon (optional emoji or letters)" maxlength="16" value="<?= e($l['icon'] ?? '') ?>">
<div class="form-check mb-2"><input class="form-check-input" type="checkbox" name="enabled" id="en-<?= e($id) ?>"<?= $on ? ' checked' : '' ?>>
<label class="form-check-label" for="en-<?= e($id) ?>">Enabled</label></div>
<div class="d-flex flex-wrap gap-1">
<button class="btn btn-accent" name="action" value="save_link">Save</button>
<a class="btn btn-outline-secondary" href="?">Cancel</a>
<?php if ($id !== 'new'): ?><button class="btn btn-outline-danger ms-auto" name="action" value="delete_link" formnovalidate onclick="return confirm('Delete this link?')">Delete</button><?php endif; ?>
</div>
</form>
<?php else: ?>
<div class="d-flex gap-3">
<span class="icon" aria-hidden="true"><?= (e($l['icon'] ?? '') ?: '🔗') ?></span>
<div>
<h2 class="h5 card-title mb-1"><a class="text-decoration-none<?= $isAdmin ? '' : ' stretched-link' ?>" href="<?= e(safe_url($l['url'] ?? '')) ?>"><?= e($l['title'] ?? '') ?></a></h2>
<p class="card-text text-body-secondary mb-0"><?= e($l['description'] ?? '') ?></p>
<?php if (!$on): ?><span class="badge text-bg-secondary mt-2">Disabled</span><?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php if ($isAdmin && $edit !== $id && $id !== ''): ?>
<div class="card-footer">
<form method="post" class="d-flex gap-1">
<input type="hidden" name="csrf" value="<?= e($csrf) ?>">
<input type="hidden" name="id" value="<?= e($id) ?>">
<button class="btn btn-sm btn-outline-secondary" name="action" value="up" aria-label="Move up">↑</button>
<button class="btn btn-sm btn-outline-secondary" name="action" value="down" aria-label="Move down">↓</button>
<button class="btn btn-sm btn-outline-secondary" name="action" value="toggle"><?= $on ? 'Disable' : 'Enable' ?></button>
<a class="btn btn-sm btn-outline-secondary ms-auto" href="?edit=<?= urlencode($id) ?>">✎ Edit</a>
</form>
</div>
<?php endif; ?>
</div></div>
<?php endforeach; ?>
<?php if ($isAdmin && $edit !== 'new'): ?>
<div class="col"><a class="card h-100 text-decoration-none" href="?edit=new"><div class="card-body d-flex align-items-center justify-content-center">+ Add link</div></a></div>
<?php endif; ?>
</div>
<?php
// Optional sections. To add a feature, create <name>/<name>.php and add 'name/name.php' to this list.
// Each file renders its own HTML, can use $config, $isAdmin and $csrf, and shares portal.css.
foreach (['chat/chat.php', 'files/files.php'] as $part) {
if (!is_file(__DIR__ . '/' . $part)) continue;
echo '<section class="feature">';
include __DIR__ . '/' . $part;
echo '</section>';
}
?>
</main>
<script src="theme-controls.js"></script>
</body>
</html>