togethere.cloud/private_html/administration/disciplines/ping-pong/index.php

824 lines
32 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
require_once __DIR__ . '/../../includes/auth.php';
require_once __DIR__ . '/../../includes/config.php';
require_once __DIR__ . '/../../includes/header.php';
require_once __DIR__ . '/../../includes/sidebar.php';
require_once __DIR__ . '/../../../api/DisciplineSettingsModel.php';
require_once __DIR__ . '/../../../api/DisciplineSettingsService.php';
$discipline = 'ping-pong';
$settingsError = null;
try {
$model = new DisciplineSettingsModel($pdo);
$service = new DisciplineSettingsService($model);
$settings = $service->getSettingsForAPI($discipline);
} catch (Throwable $e) {
error_log('Ping-Pong settings load error: ' . $e->getMessage());
$defaults = DisciplineSettingsModel::getDefaults($discipline);
$settings = [
'discipline' => $discipline,
'settingsVersion' => 0,
'rules' => [
'pointsToWin' => $defaults['pointsToWin'],
'setsToWin' => $defaults['setsToWin'],
'serveRotation' => $defaults['serveRotation'],
'specialRules' => $defaults['specialRules']
],
'customization' => $defaults['customization'] ?? [],
'metadata' => [
'created_at' => null,
'updated_at' => null,
'updated_by' => null
],
'status' => 'default'
];
$settingsError = 'Błąd wczytywania ustawień. Spróbuj odświeżyć stronę.';
}
?>
<div class="admin-content">
<style>
.admin-page-title {
font-size: 24px;
font-weight: 600;
color: #23282d;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #0073aa;
}
.settings-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-top: 30px;
}
.settings-section {
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 25px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.settings-section h2 {
font-size: 18px;
font-weight: 600;
color: #23282d;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #0073aa;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: 500;
margin-bottom: 5px;
color: #333;
font-size: 14px;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
max-height: 300px;
font-family: monospace;
}
.form-group input[type="number"] {
width: 100%;
}
.form-hint {
font-size: 12px;
color: #666;
margin-top: 3px;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 25px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
}
.btn-primary {
background: #0073aa;
color: white;
}
.btn-primary:hover {
background: #005a87;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.alert svg {
flex-shrink: 0;
width: 20px;
height: 20px;
}
.alert-success svg {
fill: #155724;
}
.alert-error svg {
fill: #721c24;
}
.alert-info svg {
fill: #0c5460;
}
/* Toasts (custom alerts) */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 9999;
}
.toast {
min-width: 280px;
max-width: 420px;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid transparent;
display: grid;
grid-template-columns: 22px 1fr auto;
align-items: center;
gap: 10px;
box-shadow: 0 10px 20px rgba(0,0,0,0.12);
animation: toast-in 180ms ease-out;
background: #fff;
}
.toast-success { border-color: #c3e6cb; background: #f6fffa; }
.toast-error { border-color: #f5c6cb; background: #fff6f6; }
.toast-info { border-color: #bee5eb; background: #f6fdff; }
.toast .icon { width: 22px; height: 22px; }
.toast-success .icon { color: #2f7a3e; }
.toast-error .icon { color: #b42318; }
.toast-info .icon { color: #0b6b8c; }
.toast .close-btn {
border: none;
background: transparent;
cursor: pointer;
color: #555;
padding: 4px;
border-radius: 6px;
}
.toast .close-btn:hover { background: rgba(0,0,0,0.06); }
@keyframes toast-in {
from { opacity: 0; transform: translateY(-6px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* Modal (custom confirm/preview) */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
display: none;
align-items: center;
justify-content: center;
z-index: 9998;
}
.modal {
width: min(720px, 92vw);
background: #fff;
border-radius: 12px;
box-shadow: 0 30px 50px rgba(0,0,0,0.25);
overflow: hidden;
animation: modal-in 160ms ease-out;
}
@keyframes modal-in { from {opacity:0; transform: translateY(8px);} to {opacity:1; transform:none;} }
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid #eee;
}
.modal-title { font-weight: 600; font-size: 16px; }
.modal-header .close-btn {
width: 34px;
height: 34px;
border: 1px solid rgba(0,0,0,0.10);
background: rgba(0,0,0,0.02);
color: #444;
border-radius: 10px;
cursor: pointer;
display: grid;
place-items: center;
font-size: 16px;
line-height: 1;
padding: 0;
transition: background 140ms ease, border-color 140ms ease, transform 140ms ease, box-shadow 140ms ease;
}
.modal-header .close-btn:hover {
background: rgba(0,0,0,0.06);
border-color: rgba(0,0,0,0.16);
}
.modal-header .close-btn:active {
transform: translateY(1px);
}
.modal-header .close-btn:focus {
outline: none;
}
.modal-header .close-btn:focus-visible {
box-shadow: 0 0 0 3px rgba(0,115,170,0.22);
border-color: rgba(0,115,170,0.55);
}
.modal-body { padding: 16px; }
.modal-actions { display:flex; gap:10px; padding: 0 16px 16px; }
.btn-outline { background:#fff; color:#333; border:1px solid #ddd; }
.btn-outline:hover { background:#f6f6f6; }
/* Inline mini preview */
.preview-wrap { display:grid; grid-template-columns: 220px 1fr; gap:14px; align-items:center; }
.mini-table {
position: relative;
width: 220px; height: 130px;
border-radius: 8px;
border: 1px solid #ddd;
overflow: hidden;
}
.mini-table .net {
position:absolute; left:50%; top:0; bottom:0; width:2px; background:rgba(255,255,255,0.7); transform: translateX(-50%);
}
.mini-table .paddle {
position:absolute; width:10px; height:36px; top:50%; transform:translateY(-50%); border-radius:3px;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.25);
}
.mini-table .paddle.left { left:8px; }
.mini-table .paddle.right { right:8px; }
.mini-table .ball { position:absolute; width:12px; height:12px; border-radius:50%; top:calc(50% - 6px); left:calc(50% - 6px); box-shadow:0 1px 2px rgba(0,0,0,0.3); }
.preview-details { font-size:12px; line-height:1.6; }
.info-box {
background: #f8f9fa;
border-left: 4px solid #0073aa;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
font-size: 13px;
}
.current-values {
background: #f8f9fa;
padding: 15px;
padding-top: 10px;
border-radius: 4px;
margin-top: 15px;
font-size: 13px;
}
.current-values dt {
font-weight: 600;
margin-top: 10px;
}
.current-values dd {
margin-left: 0;
color: #0073aa;
font-family: monospace;
}
.color-input-wrapper {
display: flex;
gap: 10px;
align-items: center;
}
input[type="color"] {
height: 40px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
@media (max-width: 1200px) {
.settings-container {
grid-template-columns: 1fr;
}
}
.loading {
opacity: 0.6;
pointer-events: none;
}
.version-info {
background: #e7f3ff;
border-left: 4px solid #0073aa;
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
font-size: 12px;
}
</style>
<h1 class="admin-page-title">🏓 Ping-Pong - Ustawienia Dyscypliny</h1>
<div id="alertContainer"></div>
<?php if (!empty($settingsError)): ?>
<div class="alert alert-error">
<?php echo htmlspecialchars($settingsError); ?>
</div>
<?php endif; ?>
<div class="info-box">
<strong> Informacja:</strong> Każda zmiana ustawień zwiększa wersję. Gry są zawsze uruchamiane z
snapshot'em ustawień z momentu startu, więc stare mecze nie są dotknięte zmianami.
</div>
<div class="version-info">
<strong>Obecna wersja:</strong> v<?php echo $settings['settingsVersion']; ?>
<br/>
<strong>Ostatnia zmiana:</strong> <?php echo $settings['metadata']['updated_at'] ?: 'brak'; ?>
</div>
<form id="settingsForm">
<div class="settings-container">
<!-- SEKCJA REGUŁ GRY -->
<div class="settings-section">
<h2>🎮 Reguły Gry (Logika)</h2>
<div class="form-group">
<label for="pointsToWin">Punkty do wygrania seta *</label>
<input
type="number"
id="pointsToWin"
name="pointsToWin"
min="1"
max="100"
value="<?php echo $settings['rules']['pointsToWin']; ?>"
required
/>
<div class="form-hint">Liczba punktów potrzebnych do wygrania seta (min: 1, max: 100)</div>
</div>
<div class="form-group">
<label for="setsToWin">Sety do wygrania meczu *</label>
<input
type="number"
id="setsToWin"
name="setsToWin"
min="1"
max="100"
value="<?php echo $settings['rules']['setsToWin']; ?>"
required
/>
<div class="form-hint">Liczba setów potrzebnych do wygrania meczu (min: 1, max: 100)</div>
</div>
<div class="form-group">
<label for="serveRotation">Punkty do zmiany serwisu *</label>
<input
type="number"
id="serveRotation"
name="serveRotation"
min="1"
max="50"
value="<?php echo $settings['rules']['serveRotation']; ?>"
required
/>
<div class="form-hint">Po ilu punktach następuje zmiana serwisu (min: 1, max: 50)</div>
</div>
<div class="form-group">
<label for="specialRules">Specjalne reguły</label>
<textarea
id="specialRules"
name="specialRules"
placeholder="np. Deuce at 10-10, brak przerw, tie-break w ostatnim secie..."
><?php echo htmlspecialchars($settings['rules']['specialRules'] ?? ''); ?></textarea>
<div class="form-hint">Dodatkowe reguły (opcjonalne)</div>
</div>
<div class="current-values">
<dt>Aktualne wartości:</dt>
<dd>pointsToWin: <?php echo $settings['rules']['pointsToWin']; ?></dd>
<dd>setsToWin: <?php echo $settings['rules']['setsToWin']; ?></dd>
<dd>serveRotation: <?php echo $settings['rules']['serveRotation']; ?></dd>
</div>
</div>
<!-- SEKCJA PERSONALIZACJI UI -->
<div class="settings-section">
<h2>🎨 Personalizacja UI</h2>
<div class="form-group">
<label for="tableColor">Kolor stołu</label>
<div class="color-input-wrapper">
<input
type="color"
id="tableColor"
name="tableColor"
value="<?php echo htmlspecialchars($settings['customization']['tableColor'] ?? '#2d5016'); ?>"
/>
<input
type="text"
name="tableColorText"
value="<?php echo htmlspecialchars($settings['customization']['tableColor'] ?? '#2d5016'); ?>"
style="flex: 1;"
readonly
/>
</div>
</div>
<div class="form-group">
<label for="ballColor">Kolor piłki</label>
<div class="color-input-wrapper">
<input
type="color"
id="ballColor"
name="ballColor"
value="<?php echo htmlspecialchars($settings['customization']['ballColor'] ?? '#ff6600'); ?>"
/>
<input
type="text"
name="ballColorText"
value="<?php echo htmlspecialchars($settings['customization']['ballColor'] ?? '#ff6600'); ?>"
style="flex: 1;"
readonly
/>
</div>
</div>
<div class="form-group">
<label for="paddleColor">Kolor rakietki</label>
<div class="color-input-wrapper">
<input
type="color"
id="paddleColor"
name="paddleColor"
value="<?php echo htmlspecialchars($settings['customization']['paddleColor'] ?? '#000000'); ?>"
/>
<input
type="text"
name="paddleColorText"
value="<?php echo htmlspecialchars($settings['customization']['paddleColor'] ?? '#000000'); ?>"
style="flex: 1;"
readonly
/>
</div>
</div>
<div class="form-group">
<label for="uiTheme">Motyw interfejsu</label>
<select id="uiTheme" name="uiTheme">
<option value="dark" <?php echo ($settings['customization']['uiTheme'] ?? 'dark') === 'dark' ? 'selected' : ''; ?>>Ciemny (Dark)</option>
<option value="light" <?php echo ($settings['customization']['uiTheme'] ?? 'light') === 'light' ? 'selected' : ''; ?>>Jasny (Light)</option>
<option value="auto">Automatyczny (Auto)</option>
</select>
<div class="form-hint">Wybierz motyw interfejsu gry</div>
</div>
<div class="current-values">
<dt>Podgląd:</dt>
<div class="preview-wrap" style="margin-top:10px;">
<div class="mini-table" id="miniTable" style="background: <?php echo htmlspecialchars($settings['customization']['tableColor'] ?? '#2d5016'); ?>;">
<div class="net"></div>
<div class="paddle left" id="miniPaddleLeft" style="background: <?php echo htmlspecialchars($settings['customization']['paddleColor'] ?? '#000000'); ?>;"></div>
<div class="paddle right" id="miniPaddleRight" style="background: <?php echo htmlspecialchars($settings['customization']['paddleColor'] ?? '#000000'); ?>;"></div>
<div class="ball" id="miniBall" style="background: <?php echo htmlspecialchars($settings['customization']['ballColor'] ?? '#ff6600'); ?>;"></div>
</div>
<div class="preview-details" id="previewDetails">
Stół: <?php echo htmlspecialchars($settings['customization']['tableColor'] ?? '#2d5016'); ?><br/>
Piłka: <?php echo htmlspecialchars($settings['customization']['ballColor'] ?? '#ff6600'); ?><br/>
Rakietka: <?php echo htmlspecialchars($settings['customization']['paddleColor'] ?? '#000000'); ?><br/>
Motyw: <?php echo htmlspecialchars($settings['customization']['uiTheme'] ?? 'dark'); ?>
</div>
</div>
</div>
</div>
</div>
<div class="button-group" style="margin-top: 30px;">
<button type="submit" class="btn btn-primary">💾 Zapisz ustawienia</button>
<button type="button" class="btn btn-secondary" onclick="resetToDefaults()">🔄 Resetuj do defaults</button>
<button type="button" class="btn btn-danger" onclick="previewChanges()">👁️ Podgląd zmian</button>
</div>
</form>
</div>
<script>
const settingsEndpoint = '/administration/disciplines/ping-pong/settings/';
// Synchronizuj kolory
document.getElementById('tableColor')?.addEventListener('change', function() {
document.querySelector('input[name="tableColorText"]').value = this.value;
});
document.getElementById('ballColor')?.addEventListener('change', function() {
document.querySelector('input[name="ballColorText"]').value = this.value;
});
document.getElementById('paddleColor')?.addEventListener('change', function() {
document.querySelector('input[name="paddleColorText"]').value = this.value;
});
// Obsługa formularza
document.getElementById('settingsForm')?.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = {
rules: {
pointsToWin: parseInt(document.getElementById('pointsToWin').value),
setsToWin: parseInt(document.getElementById('setsToWin').value),
serveRotation: parseInt(document.getElementById('serveRotation').value),
specialRules: document.getElementById('specialRules').value || null
},
customization: {
tableColor: document.getElementById('tableColor').value,
ballColor: document.getElementById('ballColor').value,
paddleColor: document.getElementById('paddleColor').value,
uiTheme: document.getElementById('uiTheme').value
}
};
try {
const response = await fetch(settingsEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
// Odśwież stronę po 2 sekundach
setTimeout(() => location.reload(), 2000);
} else {
showAlert(result.message, 'error');
}
} catch (error) {
showAlert('Błąd: ' + error.message, 'error');
}
});
function resetToDefaults() {
confirmDialog('Jesteś pewny? Ustawienia zostaną zresetowane do defaults.')
.then(ok => {
if (!ok) return;
fetch(settingsEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reset: true })
})
.then(r => r.json())
.then(data => {
if (data.success) {
showAlert(data.message, 'success');
setTimeout(() => location.reload(), 2000);
} else {
showAlert(data.message, 'error');
}
})
.catch(e => showAlert('Błąd: ' + e.message, 'error'));
});
}
function previewChanges() {
const rules = {
pointsToWin: parseInt(document.getElementById('pointsToWin').value),
setsToWin: parseInt(document.getElementById('setsToWin').value),
serveRotation: parseInt(document.getElementById('serveRotation').value)
};
openPreviewModal(rules);
}
function showAlert(message, type) {
let container = document.getElementById('toastContainer');
if (!container) {
container = document.createElement('div');
container.id = 'toastContainer';
container.className = 'toast-container';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<span class="icon">${type === 'success' ? '✅' : type === 'error' ? '⛔' : ''}</span>
<div>${message}</div>
<button class="close-btn" aria-label="Zamknij">✖</button>
`;
toast.querySelector('.close-btn').addEventListener('click', () => toast.remove());
container.appendChild(toast);
setTimeout(() => toast.remove(), 5000);
}
// Custom confirm dialog returning a Promise
function confirmDialog(text) {
return new Promise((resolve) => {
let overlay = document.getElementById('modalOverlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'modalOverlay';
overlay.className = 'modal-overlay';
overlay.innerHTML = `
<div class="modal" role="dialog" aria-modal="true">
<div class="modal-header">
<div class="modal-title">Potwierdzenie</div>
<button class="close-btn" aria-label="Zamknij">✖</button>
</div>
<div class="modal-body" id="modalBody"></div>
<div class="modal-actions">
<button class="btn btn-outline" id="modalCancel">Anuluj</button>
<button class="btn btn-primary" id="modalOk">Potwierdź</button>
</div>
</div>`;
document.body.appendChild(overlay);
}
overlay.style.display = 'flex';
overlay.querySelector('#modalBody').textContent = text;
const close = () => overlay.style.display = 'none';
const okBtn = overlay.querySelector('#modalOk');
const cancelBtn = overlay.querySelector('#modalCancel');
const xBtn = overlay.querySelector('.close-btn');
const cleanup = () => {
okBtn.onclick = null; cancelBtn.onclick = null; xBtn.onclick = null;
};
okBtn.onclick = () => { cleanup(); close(); resolve(true); };
cancelBtn.onclick = () => { cleanup(); close(); resolve(false); };
xBtn.onclick = () => { cleanup(); close(); resolve(false); };
});
}
// Modal preview showing table and rules
function openPreviewModal(rules) {
let overlay = document.getElementById('previewOverlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'previewOverlay';
overlay.className = 'modal-overlay';
overlay.innerHTML = `
<div class="modal" role="dialog" aria-modal="true">
<div class="modal-header">
<div class="modal-title">Podgląd zmian</div>
<button class="close-btn" aria-label="Zamknij">✖</button>
</div>
<div class="modal-body">
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:16px; align-items:center;">
<div class="mini-table" id="previewTable" style="width:100%; height:220px;"></div>
<div id="previewInfo" style="font-size:14px; line-height:1.7;"></div>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.style.display = 'none'; });
overlay.querySelector('.close-btn').addEventListener('click', () => overlay.style.display = 'none');
}
const tableColor = document.getElementById('tableColor').value;
const ballColor = document.getElementById('ballColor').value;
const paddleColor = document.getElementById('paddleColor').value;
const uiTheme = document.getElementById('uiTheme').value;
const table = overlay.querySelector('#previewTable');
table.innerHTML = `
<div class="net"></div>
<div class="paddle left" style="width:14px;height:60px;background:${paddleColor};left:12px;"></div>
<div class="paddle right" style="width:14px;height:60px;background:${paddleColor};right:12px;"></div>
<div class="ball" style="width:18px;height:18px;background:${ballColor};top:calc(50% - 9px);left:calc(50% - 9px);"></div>
`;
table.style.background = tableColor;
const info = overlay.querySelector('#previewInfo');
info.innerHTML = `
<strong>Reguły gry</strong><br/>
Punkty do seta: <b>${rules.pointsToWin}</b><br/>
Sety do meczu: <b>${rules.setsToWin}</b><br/>
Zmiana serwisu: co <b>${rules.serveRotation}</b> pkt<br/>
<br/>
<strong>Wygląd</strong><br/>
Stół: <span style="font-family:monospace">${tableColor}</span><br/>
Piłka: <span style="font-family:monospace">${ballColor}</span><br/>
Rakietka: <span style="font-family:monospace">${paddleColor}</span><br/>
Motyw: <b>${uiTheme}</b>
`;
overlay.style.display = 'flex';
}
// Live inline preview updates
function updateInlinePreview() {
const tableColor = document.getElementById('tableColor').value;
const ballColor = document.getElementById('ballColor').value;
const paddleColor = document.getElementById('paddleColor').value;
const uiTheme = document.getElementById('uiTheme').value;
const miniTable = document.getElementById('miniTable');
if (miniTable) {
miniTable.style.background = tableColor;
const left = document.getElementById('miniPaddleLeft');
const right = document.getElementById('miniPaddleRight');
const ball = document.getElementById('miniBall');
if (left) left.style.background = paddleColor;
if (right) right.style.background = paddleColor;
if (ball) ball.style.background = ballColor;
}
const details = document.getElementById('previewDetails');
if (details) {
details.innerHTML = `Stół: ${tableColor}<br/>Piłka: ${ballColor}<br/>Rakietka: ${paddleColor}<br/>Motyw: ${uiTheme}`;
}
}
['tableColor','ballColor','paddleColor','uiTheme','pointsToWin','setsToWin','serveRotation','specialRules']
.forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('input', updateInlinePreview); });
updateInlinePreview();
</script>
<?php require_once __DIR__ . '/../../includes/footer.php'; ?>