Auto commit: 2026-04-15T16:05:12.532Z
This commit is contained in:
parent
72bfa75c68
commit
1c7765e1cd
340
admin.php
340
admin.php
@ -3,6 +3,10 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/urban_hikes.php';
|
||||
|
||||
$storage = urban_hikes_storage();
|
||||
$routeId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$editingRoute = $routeId > 0 ? urban_hikes_find($routeId) : null;
|
||||
$isEditMode = $editingRoute !== null;
|
||||
$missingRoute = $routeId > 0 && !$editingRoute;
|
||||
$errors = [];
|
||||
$formData = [
|
||||
'city' => '',
|
||||
@ -18,21 +22,44 @@ $formData = [
|
||||
'best_for' => '',
|
||||
];
|
||||
|
||||
if ($editingRoute) {
|
||||
$formData = array_merge($formData, $editingRoute);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$result = urban_hikes_create($_POST);
|
||||
$postedId = isset($_POST['route_id']) ? (int)$_POST['route_id'] : 0;
|
||||
if ($postedId > 0) {
|
||||
$result = urban_hikes_update($postedId, $_POST);
|
||||
$routeId = $postedId;
|
||||
$isEditMode = true;
|
||||
} else {
|
||||
$result = urban_hikes_create($_POST);
|
||||
$routeId = 0;
|
||||
$isEditMode = false;
|
||||
}
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
urban_hikes_set_flash('success', 'Route saved and published to the directory.');
|
||||
urban_hikes_set_flash('success', $postedId > 0 ? 'Route updated successfully.' : 'Route saved and published to the directory.');
|
||||
header('Location: route.php?id=' . (int)$result['id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$errors = $result['errors'] ?? [];
|
||||
$formData = array_merge($formData, $result['input'] ?? []);
|
||||
$editingRoute = $routeId > 0 ? urban_hikes_find($routeId) : null;
|
||||
$missingRoute = $routeId > 0 && !$editingRoute;
|
||||
}
|
||||
|
||||
$latestRoutes = urban_hikes_latest();
|
||||
$pageTitle = 'Add a route | ' . urban_hikes_project_name();
|
||||
$pageDescription = 'Add an urban hiking route with city, difficulty, distance, highlights, and map link.';
|
||||
if ($missingRoute) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
$latestRoutes = urban_hikes_latest(10);
|
||||
$pageTitle = ($isEditMode ? 'Edit route' : 'Add a route') . ' | ' . urban_hikes_project_name();
|
||||
$pageDescription = $isEditMode
|
||||
? 'Update an existing urban hiking route and republish the latest details.'
|
||||
: 'Add an urban hiking route with city, difficulty, distance, highlights, and map link.';
|
||||
$submitLabel = $isEditMode ? 'Save changes' : 'Publish route';
|
||||
|
||||
urban_hikes_render_head($pageTitle, $pageDescription, 'noindex, follow');
|
||||
urban_hikes_render_nav('admin');
|
||||
@ -40,147 +67,182 @@ urban_hikes_render_nav('admin');
|
||||
<main>
|
||||
<section class="section-shell border-bottom">
|
||||
<div class="container-lg px-3 px-lg-4 py-4 py-lg-5">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-7">
|
||||
<div class="panel-card">
|
||||
<span class="eyebrow">Content admin</span>
|
||||
<h1 class="section-title mt-2 mb-2">Add a new urban route</h1>
|
||||
<p class="text-muted mb-4">This first admin screen keeps the workflow small: add a route once, then browse it in the public directory immediately.</p>
|
||||
|
||||
<?php if (!$storage['ready']): ?>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Database saving is unavailable right now, so new routes cannot be published yet.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($errors['storage'])): ?>
|
||||
<div class="alert alert-warning" role="alert"><?= htmlspecialchars($errors['storage']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="admin.php" class="row g-3" novalidate>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="city">City</label>
|
||||
<input class="form-control <?= isset($errors['city']) ? 'is-invalid' : '' ?>" type="text" id="city" name="city" value="<?= htmlspecialchars($formData['city']) ?>" placeholder="Berlin" required />
|
||||
<?php if (isset($errors['city'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['city']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="difficulty">Difficulty</label>
|
||||
<select class="form-select <?= isset($errors['difficulty']) ? 'is-invalid' : '' ?>" id="difficulty" name="difficulty">
|
||||
<?php foreach (['Easy', 'Moderate', 'Challenging'] as $level): ?>
|
||||
<option value="<?= htmlspecialchars($level) ?>" <?= $formData['difficulty'] === $level ? 'selected' : '' ?>><?= htmlspecialchars($level) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['difficulty'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['difficulty']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="title">Route title</label>
|
||||
<input class="form-control <?= isset($errors['title']) ? 'is-invalid' : '' ?>" type="text" id="title" name="title" value="<?= htmlspecialchars($formData['title']) ?>" placeholder="Hilltop parks to market streets walk" required />
|
||||
<?php if (isset($errors['title'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['title']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="distance_km">Distance (km)</label>
|
||||
<input class="form-control <?= isset($errors['distance_km']) ? 'is-invalid' : '' ?>" type="number" min="0.1" step="0.1" id="distance_km" name="distance_km" value="<?= htmlspecialchars($formData['distance_km']) ?>" required />
|
||||
<?php if (isset($errors['distance_km'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['distance_km']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="duration_hours">Duration (hours)</label>
|
||||
<input class="form-control <?= isset($errors['duration_hours']) ? 'is-invalid' : '' ?>" type="number" min="0.1" step="0.1" id="duration_hours" name="duration_hours" value="<?= htmlspecialchars($formData['duration_hours']) ?>" required />
|
||||
<?php if (isset($errors['duration_hours'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['duration_hours']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="neighborhood">Neighborhood / area</label>
|
||||
<input class="form-control <?= isset($errors['neighborhood']) ? 'is-invalid' : '' ?>" type="text" id="neighborhood" name="neighborhood" value="<?= htmlspecialchars($formData['neighborhood']) ?>" placeholder="Waterfront district" required />
|
||||
<?php if (isset($errors['neighborhood'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['neighborhood']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="start_point">Start point</label>
|
||||
<input class="form-control <?= isset($errors['start_point']) ? 'is-invalid' : '' ?>" type="text" id="start_point" name="start_point" value="<?= htmlspecialchars($formData['start_point']) ?>" placeholder="Central Station" required />
|
||||
<?php if (isset($errors['start_point'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['start_point']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label d-flex justify-content-between align-items-center" for="summary">
|
||||
<span>Summary</span>
|
||||
<span class="field-count" id="summaryCount">0 chars</span>
|
||||
</label>
|
||||
<textarea class="form-control <?= isset($errors['summary']) ? 'is-invalid' : '' ?>" id="summary" name="summary" rows="3" placeholder="What makes this route worth a half day?" data-count-target="summaryCount"><?= htmlspecialchars($formData['summary']) ?></textarea>
|
||||
<?php if (isset($errors['summary'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['summary']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label d-flex justify-content-between align-items-center" for="highlights">
|
||||
<span>Highlights</span>
|
||||
<span class="field-count" id="highlightsCount">0 chars</span>
|
||||
</label>
|
||||
<textarea class="form-control <?= isset($errors['highlights']) ? 'is-invalid' : '' ?>" id="highlights" name="highlights" rows="4" placeholder="One highlight per line" data-count-target="highlightsCount"><?= htmlspecialchars($formData['highlights']) ?></textarea>
|
||||
<div class="form-text">Enter one stop or viewpoint per line.</div>
|
||||
<?php if (isset($errors['highlights'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['highlights']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label" for="map_url">Map link</label>
|
||||
<input class="form-control <?= isset($errors['map_url']) ? 'is-invalid' : '' ?>" type="url" id="map_url" name="map_url" value="<?= htmlspecialchars($formData['map_url']) ?>" placeholder="https://maps.google.com/..." required />
|
||||
<?php if (isset($errors['map_url'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['map_url']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label" for="best_for">Best for</label>
|
||||
<input class="form-control <?= isset($errors['best_for']) ? 'is-invalid' : '' ?>" type="text" id="best_for" name="best_for" value="<?= htmlspecialchars($formData['best_for']) ?>" placeholder="Early morning walkers" required />
|
||||
<?php if (isset($errors['best_for'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['best_for']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12 d-flex gap-2 pt-2">
|
||||
<button class="btn btn-dark" type="submit" <?= !$storage['ready'] ? 'disabled' : '' ?>>Publish route</button>
|
||||
<a class="btn btn-outline-secondary" href="index.php#results">Back to directory</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php if ($missingRoute): ?>
|
||||
<div class="panel-card empty-state text-center">
|
||||
<span class="eyebrow">Route missing</span>
|
||||
<h1 class="section-title mt-2 mb-2">That route could not be opened for editing.</h1>
|
||||
<p class="text-muted mb-4">Try returning to the directory and opening a valid route first.</p>
|
||||
<div class="d-flex flex-column flex-sm-row gap-2 justify-content-center">
|
||||
<a class="btn btn-dark" href="index.php#results">Back to directory</a>
|
||||
<a class="btn btn-outline-secondary" href="admin.php">Create a new route instead</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-7">
|
||||
<div class="panel-card">
|
||||
<span class="eyebrow">Content admin</span>
|
||||
<h1 class="section-title mt-2 mb-2"><?= $isEditMode ? 'Edit urban route' : 'Add a new urban route' ?></h1>
|
||||
<p class="text-muted mb-4">
|
||||
<?= $isEditMode
|
||||
? 'Update the route details below. After saving, the public directory and route page will immediately show the latest version.'
|
||||
: 'This admin screen keeps the workflow small: add a route once, then browse it in the public directory immediately.' ?>
|
||||
</p>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<div class="panel-card mb-3">
|
||||
<span class="eyebrow">Workflow</span>
|
||||
<h2 class="section-title h5 mt-2">What this first slice covers</h2>
|
||||
<ul class="compact-list mb-0 mt-3">
|
||||
<li>Add a route with the key planning fields</li>
|
||||
<li>Store it in MariaDB with prepared statements</li>
|
||||
<li>Redirect to a detail page with a confirmation toast</li>
|
||||
<li>See it in the public directory immediately</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="panel-card">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<span class="eyebrow">Latest entries</span>
|
||||
<h2 class="section-title h5 mt-2 mb-0">Recent routes</h2>
|
||||
</div>
|
||||
<a class="small text-decoration-none" href="index.php#results">Open directory</a>
|
||||
</div>
|
||||
<?php if (!$latestRoutes): ?>
|
||||
<p class="text-muted mb-0">No routes published yet.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle admin-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Route</th>
|
||||
<th>City</th>
|
||||
<th class="text-end">View</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($latestRoutes as $route): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= htmlspecialchars($route['title']) ?></div>
|
||||
<div class="text-muted small"><?= htmlspecialchars($route['difficulty']) ?> · <?= htmlspecialchars(number_format((float)$route['distance_km'], 1)) ?> km</div>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($route['city']) ?></td>
|
||||
<td class="text-end"><a class="btn btn-sm btn-outline-secondary" href="route.php?id=<?= (int)$route['id'] ?>">Open</a></td>
|
||||
</tr>
|
||||
<?php if (!$storage['ready']): ?>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Database saving is unavailable right now, so changes cannot be published yet.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($errors['storage'])): ?>
|
||||
<div class="alert alert-warning" role="alert"><?= htmlspecialchars($errors['storage']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="admin.php<?= $isEditMode ? '?id=' . (int)$routeId : '' ?>" class="row g-3" novalidate>
|
||||
<?php if ($isEditMode): ?>
|
||||
<input type="hidden" name="route_id" value="<?= (int)$routeId ?>" />
|
||||
<?php endif; ?>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="city">City</label>
|
||||
<input class="form-control <?= isset($errors['city']) ? 'is-invalid' : '' ?>" type="text" id="city" name="city" value="<?= htmlspecialchars((string)$formData['city']) ?>" placeholder="Berlin" required />
|
||||
<?php if (isset($errors['city'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['city']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="difficulty">Difficulty</label>
|
||||
<select class="form-select <?= isset($errors['difficulty']) ? 'is-invalid' : '' ?>" id="difficulty" name="difficulty">
|
||||
<?php foreach (['Easy', 'Moderate', 'Challenging'] as $level): ?>
|
||||
<option value="<?= htmlspecialchars($level) ?>" <?= (string)$formData['difficulty'] === $level ? 'selected' : '' ?>><?= htmlspecialchars($level) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</select>
|
||||
<?php if (isset($errors['difficulty'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['difficulty']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="title">Route title</label>
|
||||
<input class="form-control <?= isset($errors['title']) ? 'is-invalid' : '' ?>" type="text" id="title" name="title" value="<?= htmlspecialchars((string)$formData['title']) ?>" placeholder="Hilltop parks to market streets walk" required />
|
||||
<?php if (isset($errors['title'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['title']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="distance_km">Distance (km)</label>
|
||||
<input class="form-control <?= isset($errors['distance_km']) ? 'is-invalid' : '' ?>" type="number" min="0.1" step="0.1" id="distance_km" name="distance_km" value="<?= htmlspecialchars((string)$formData['distance_km']) ?>" required />
|
||||
<?php if (isset($errors['distance_km'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['distance_km']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="duration_hours">Duration (hours)</label>
|
||||
<input class="form-control <?= isset($errors['duration_hours']) ? 'is-invalid' : '' ?>" type="number" min="0.1" step="0.1" id="duration_hours" name="duration_hours" value="<?= htmlspecialchars((string)$formData['duration_hours']) ?>" required />
|
||||
<?php if (isset($errors['duration_hours'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['duration_hours']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="neighborhood">Neighborhood / area</label>
|
||||
<input class="form-control <?= isset($errors['neighborhood']) ? 'is-invalid' : '' ?>" type="text" id="neighborhood" name="neighborhood" value="<?= htmlspecialchars((string)$formData['neighborhood']) ?>" placeholder="Waterfront district" required />
|
||||
<?php if (isset($errors['neighborhood'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['neighborhood']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="start_point">Start point</label>
|
||||
<input class="form-control <?= isset($errors['start_point']) ? 'is-invalid' : '' ?>" type="text" id="start_point" name="start_point" value="<?= htmlspecialchars((string)$formData['start_point']) ?>" placeholder="Central Station" required />
|
||||
<?php if (isset($errors['start_point'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['start_point']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label d-flex justify-content-between align-items-center" for="summary">
|
||||
<span>Summary</span>
|
||||
<span class="field-count" id="summaryCount">0 chars</span>
|
||||
</label>
|
||||
<textarea class="form-control <?= isset($errors['summary']) ? 'is-invalid' : '' ?>" id="summary" name="summary" rows="3" placeholder="What makes this route worth a half day?" data-count-target="summaryCount"><?= htmlspecialchars((string)$formData['summary']) ?></textarea>
|
||||
<?php if (isset($errors['summary'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['summary']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label d-flex justify-content-between align-items-center" for="highlights">
|
||||
<span>Highlights</span>
|
||||
<span class="field-count" id="highlightsCount">0 chars</span>
|
||||
</label>
|
||||
<textarea class="form-control <?= isset($errors['highlights']) ? 'is-invalid' : '' ?>" id="highlights" name="highlights" rows="4" placeholder="One highlight per line" data-count-target="highlightsCount"><?= htmlspecialchars((string)$formData['highlights']) ?></textarea>
|
||||
<div class="form-text">Enter one stop or viewpoint per line.</div>
|
||||
<?php if (isset($errors['highlights'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['highlights']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label" for="map_url">Map link</label>
|
||||
<input class="form-control <?= isset($errors['map_url']) ? 'is-invalid' : '' ?>" type="url" id="map_url" name="map_url" value="<?= htmlspecialchars((string)$formData['map_url']) ?>" placeholder="https://maps.google.com/..." required />
|
||||
<?php if (isset($errors['map_url'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['map_url']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label" for="best_for">Best for</label>
|
||||
<input class="form-control <?= isset($errors['best_for']) ? 'is-invalid' : '' ?>" type="text" id="best_for" name="best_for" value="<?= htmlspecialchars((string)$formData['best_for']) ?>" placeholder="Early morning walkers" required />
|
||||
<?php if (isset($errors['best_for'])): ?><div class="invalid-feedback"><?= htmlspecialchars($errors['best_for']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12 d-flex gap-2 pt-2">
|
||||
<button class="btn btn-dark" type="submit" <?= !$storage['ready'] ? 'disabled' : '' ?>><?= htmlspecialchars($submitLabel) ?></button>
|
||||
<?php if ($isEditMode): ?>
|
||||
<a class="btn btn-outline-secondary" href="route.php?id=<?= (int)$routeId ?>">Cancel</a>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-outline-secondary" href="index.php#results">Back to directory</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<div class="panel-card mb-3">
|
||||
<span class="eyebrow"><?= $isEditMode ? 'Editing' : 'Workflow' ?></span>
|
||||
<h2 class="section-title h5 mt-2"><?= $isEditMode ? 'What changes update immediately' : 'What this slice covers' ?></h2>
|
||||
<ul class="compact-list mb-0 mt-3">
|
||||
<?php if ($isEditMode): ?>
|
||||
<li>Update route content without touching the database manually</li>
|
||||
<li>Keep validation and safe prepared statements in one place</li>
|
||||
<li>Return to the public route page after saving</li>
|
||||
<li>Refresh the route everywhere it appears in the directory</li>
|
||||
<?php else: ?>
|
||||
<li>Add a route with the key planning fields</li>
|
||||
<li>Store it in MariaDB with prepared statements</li>
|
||||
<li>Redirect to a detail page with a confirmation toast</li>
|
||||
<li>See it in the public directory immediately</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="panel-card">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<span class="eyebrow">Manage routes</span>
|
||||
<h2 class="section-title h5 mt-2 mb-0">Recent routes</h2>
|
||||
</div>
|
||||
<a class="small text-decoration-none" href="index.php#results">Open directory</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!$latestRoutes): ?>
|
||||
<p class="text-muted mb-0">No routes published yet.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle admin-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Route</th>
|
||||
<th>City</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($latestRoutes as $route): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= htmlspecialchars((string)$route['title']) ?></div>
|
||||
<div class="text-muted small"><?= htmlspecialchars((string)$route['difficulty']) ?> · <?= htmlspecialchars(number_format((float)$route['distance_km'], 1)) ?> km</div>
|
||||
</td>
|
||||
<td><a class="text-decoration-none" href="city.php?city=<?= rawurlencode((string)$route['city']) ?>"><?= htmlspecialchars((string)$route['city']) ?></a></td>
|
||||
<td class="text-end">
|
||||
<div class="d-inline-flex gap-2 flex-wrap justify-content-end">
|
||||
<a class="btn btn-sm btn-outline-secondary" href="admin.php?id=<?= (int)$route['id'] ?>">Edit</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="route.php?id=<?= (int)$route['id'] ?>">Open</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
191
city.php
Normal file
191
city.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/urban_hikes.php';
|
||||
|
||||
$city = trim((string)($_GET['city'] ?? ''));
|
||||
$routes = $city !== '' ? urban_hikes_find_by_city($city) : [];
|
||||
$cityStats = $city !== '' ? urban_hikes_city_stats($city) : null;
|
||||
$allCities = urban_hikes_cities();
|
||||
$otherCities = array_values(array_filter($allCities, static function (array $item) use ($city): bool {
|
||||
return (string)($item['city'] ?? '') !== $city;
|
||||
}));
|
||||
|
||||
if (!$cityStats) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
$routeCount = $cityStats ? (int)$cityStats['route_count'] : 0;
|
||||
$avgDistance = $cityStats ? number_format((float)$cityStats['avg_distance'], 1) : '0.0';
|
||||
$avgDuration = $cityStats ? number_format((float)$cityStats['avg_duration'], 1) : '0.0';
|
||||
$areaCount = $cityStats ? (int)$cityStats['area_count'] : 0;
|
||||
$pageTitle = $cityStats
|
||||
? 'Urban hiking in ' . $cityStats['city'] . ' | ' . urban_hikes_project_name()
|
||||
: 'City not found | ' . urban_hikes_project_name();
|
||||
$pageDescription = $cityStats
|
||||
? 'Browse ' . $routeCount . ' urban hiking route' . ($routeCount === 1 ? '' : 's') . ' in ' . $cityStats['city'] . ', compare difficulty, distance, and route highlights, and plan a full walking day.'
|
||||
: 'The requested city guide could not be found.';
|
||||
$shortestRoute = $routes ? $routes[0] : null;
|
||||
$longestRoute = $routes ? $routes[count($routes) - 1] : null;
|
||||
|
||||
urban_hikes_render_head($pageTitle, $pageDescription, $cityStats ? 'index, follow' : 'noindex, nofollow');
|
||||
urban_hikes_render_nav('cities');
|
||||
?>
|
||||
<main>
|
||||
<section class="hero-shell border-bottom">
|
||||
<div class="container-lg px-3 px-lg-4 py-4 py-lg-5">
|
||||
<?php if (!$cityStats): ?>
|
||||
<div class="panel-card empty-state text-center">
|
||||
<span class="eyebrow">City missing</span>
|
||||
<h1 class="section-title mt-2 mb-2">We could not find that city guide.</h1>
|
||||
<p class="text-muted mb-4">Try opening a city from the directory instead.</p>
|
||||
<a class="btn btn-dark" href="index.php#cities">Back to cities</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb small mb-0">
|
||||
<li class="breadcrumb-item"><a href="index.php">Directory</a></li>
|
||||
<li class="breadcrumb-item"><a href="index.php#cities">Cities</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><?= htmlspecialchars((string)$cityStats['city']) ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row g-4 align-items-end">
|
||||
<div class="col-lg-7">
|
||||
<span class="eyebrow">City guide</span>
|
||||
<h1 class="display-title mt-3 mb-3">Urban hiking in <?= htmlspecialchars((string)$cityStats['city']) ?></h1>
|
||||
<p class="lead-copy mb-4">
|
||||
Use this city page as your quick planner: compare route length, difficulty, starting point, and route highlights before heading out.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-dark px-4" href="#city-routes">Browse routes</a>
|
||||
<a class="btn btn-outline-secondary px-4" href="index.php?city=<?= rawurlencode((string)$cityStats['city']) ?>#results">Open filtered directory</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="panel-card stats-panel h-100">
|
||||
<div class="row g-3">
|
||||
<div class="col-6 col-md-3 col-lg-6">
|
||||
<p class="metric-label">Routes</p>
|
||||
<p class="metric-value"><?= $routeCount ?></p>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-lg-6">
|
||||
<p class="metric-label">Avg km</p>
|
||||
<p class="metric-value"><?= htmlspecialchars($avgDistance) ?></p>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-lg-6">
|
||||
<p class="metric-label">Avg hours</p>
|
||||
<p class="metric-value"><?= htmlspecialchars($avgDuration) ?></p>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-lg-6">
|
||||
<p class="metric-label">Areas</p>
|
||||
<p class="metric-value"><?= $areaCount ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider my-4"></div>
|
||||
<ul class="compact-list mb-0">
|
||||
<li><?= (int)$cityStats['easy_count'] ?> easy · <?= (int)$cityStats['moderate_count'] ?> moderate · <?= (int)$cityStats['challenging_count'] ?> challenging</li>
|
||||
<?php if ($shortestRoute): ?>
|
||||
<li>Shortest option: <?= htmlspecialchars((string)$shortestRoute['title']) ?> at <?= htmlspecialchars(number_format((float)$shortestRoute['distance_km'], 1)) ?> km</li>
|
||||
<?php endif; ?>
|
||||
<?php if ($longestRoute && $longestRoute !== $shortestRoute): ?>
|
||||
<li>Longest option: <?= htmlspecialchars((string)$longestRoute['title']) ?> at <?= htmlspecialchars(number_format((float)$longestRoute['distance_km'], 1)) ?> km</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($cityStats): ?>
|
||||
<section class="section-shell border-bottom" id="city-routes">
|
||||
<div class="container-lg px-3 px-lg-4 py-4 py-lg-5">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-end gap-3 mb-3">
|
||||
<div>
|
||||
<span class="eyebrow"><?= htmlspecialchars((string)$cityStats['city']) ?> routes</span>
|
||||
<h2 class="section-title mb-1 mt-2">Pick the walk that fits your day.</h2>
|
||||
<p class="text-muted mb-0">Every route below links to a full detail page and an external map.</p>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary btn-sm align-self-start align-self-md-auto" href="admin.php">Add a route</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<?php foreach ($routes as $route): ?>
|
||||
<?php $highlights = array_slice(urban_hikes_highlight_items((string)$route['highlights']), 0, 3); ?>
|
||||
<div class="col-12">
|
||||
<article class="panel-card route-card h-100">
|
||||
<div class="d-flex flex-column flex-lg-row gap-3 justify-content-between">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<span class="badge text-bg-light border"><?= htmlspecialchars((string)$route['difficulty']) ?></span>
|
||||
<span class="badge text-bg-light border"><?= htmlspecialchars((string)$route['neighborhood']) ?></span>
|
||||
<span class="badge text-bg-light border">Start: <?= htmlspecialchars((string)$route['start_point']) ?></span>
|
||||
</div>
|
||||
<h2 class="route-title mb-2"><a href="route.php?id=<?= (int)$route['id'] ?>"><?= htmlspecialchars((string)$route['title']) ?></a></h2>
|
||||
<p class="text-muted mb-3"><?= htmlspecialchars((string)$route['summary']) ?></p>
|
||||
<div class="route-meta mb-3">
|
||||
<span><?= htmlspecialchars(number_format((float)$route['distance_km'], 1)) ?> km</span>
|
||||
<span><?= htmlspecialchars(number_format((float)$route['duration_hours'], 1)) ?> hr</span>
|
||||
<span><?= htmlspecialchars((string)$route['best_for']) ?></span>
|
||||
</div>
|
||||
<?php if ($highlights): ?>
|
||||
<ul class="highlights-list mb-0">
|
||||
<?php foreach ($highlights as $item): ?>
|
||||
<li><?= htmlspecialchars($item) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="route-actions d-flex flex-lg-column justify-content-between gap-2">
|
||||
<a class="btn btn-dark" href="route.php?id=<?= (int)$route['id'] ?>">View details</a>
|
||||
<a class="btn btn-outline-secondary" href="admin.php?id=<?= (int)$route['id'] ?>">Edit route</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= htmlspecialchars((string)$route['map_url']) ?>" target="_blank" rel="noopener">Open map</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="panel-card mb-3">
|
||||
<span class="eyebrow">Planning notes</span>
|
||||
<h2 class="section-title h5 mt-2">How to use this city page</h2>
|
||||
<ul class="compact-list mb-0 mt-3">
|
||||
<li>Start with distance and duration to match your available time.</li>
|
||||
<li>Open a route map only after the route summary looks right.</li>
|
||||
<li>Use the filtered directory view if you want broader search controls.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="panel-card">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<span class="eyebrow">Explore more</span>
|
||||
<h2 class="section-title h5 mt-2 mb-0">Other city guides</h2>
|
||||
</div>
|
||||
<a class="small text-decoration-none" href="index.php#cities">All cities</a>
|
||||
</div>
|
||||
<?php if (!$otherCities): ?>
|
||||
<p class="text-muted mb-0">Add another city route to expand the guide.</p>
|
||||
<?php else: ?>
|
||||
<div class="vstack gap-3">
|
||||
<?php foreach (array_slice($otherCities, 0, 5) as $cityRow): ?>
|
||||
<a class="subroute-link" href="city.php?city=<?= rawurlencode((string)$cityRow['city']) ?>">
|
||||
<strong><?= htmlspecialchars((string)$cityRow['city']) ?></strong>
|
||||
<span><?= (int)$cityRow['route_count'] ?> route<?= (int)$cityRow['route_count'] === 1 ? '' : 's' ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
<?php urban_hikes_render_footer(); ?>
|
||||
@ -173,6 +173,7 @@ urban_hikes_render_nav('directory');
|
||||
</div>
|
||||
<div class="route-actions d-flex flex-lg-column justify-content-between gap-2">
|
||||
<a class="btn btn-dark" href="route.php?id=<?= (int)$route['id'] ?>">View details</a>
|
||||
<a class="btn btn-outline-secondary" href="city.php?city=<?= rawurlencode((string)$route['city']) ?>">City guide</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= htmlspecialchars($route['map_url']) ?>" target="_blank" rel="noopener">Open map</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -198,7 +199,7 @@ urban_hikes_render_nav('directory');
|
||||
<div class="row g-3">
|
||||
<?php foreach ($cities as $cityRow): ?>
|
||||
<div class="col-6 col-md-4 col-xl-2">
|
||||
<a class="city-card panel-card h-100 d-block text-decoration-none" href="index.php?city=<?= rawurlencode((string)$cityRow['city']) ?>#results">
|
||||
<a class="city-card panel-card h-100 d-block text-decoration-none" href="city.php?city=<?= rawurlencode((string)$cityRow['city']) ?>">
|
||||
<span class="city-name"><?= htmlspecialchars($cityRow['city']) ?></span>
|
||||
<span class="city-count"><?= (int)$cityRow['route_count'] ?> route<?= (int)$cityRow['route_count'] === 1 ? '' : 's' ?></span>
|
||||
</a>
|
||||
|
||||
@ -42,7 +42,7 @@ urban_hikes_render_nav();
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb small mb-0">
|
||||
<li class="breadcrumb-item"><a href="index.php">Directory</a></li>
|
||||
<li class="breadcrumb-item"><a href="index.php?city=<?= rawurlencode((string)$route['city']) ?>#results"><?= htmlspecialchars($route['city']) ?></a></li>
|
||||
<li class="breadcrumb-item"><a href="city.php?city=<?= rawurlencode((string)$route['city']) ?>"><?= htmlspecialchars($route['city']) ?></a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><?= htmlspecialchars($route['title']) ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
@ -105,7 +105,11 @@ urban_hikes_render_nav();
|
||||
<span class="eyebrow">Next move</span>
|
||||
<h2 class="section-title h5 mt-2">Want to add another city route?</h2>
|
||||
<p class="text-muted mb-4">Use the lightweight admin screen to submit a new urban hike and make it searchable right away.</p>
|
||||
<a class="btn btn-outline-secondary w-100" href="admin.php">Add a route</a>
|
||||
<div class="d-grid gap-2">
|
||||
<a class="btn btn-outline-secondary" href="city.php?city=<?= rawurlencode((string)$route['city']) ?>">Open city guide</a>
|
||||
<a class="btn btn-outline-secondary" href="admin.php?id=<?= (int)$route['id'] ?>">Edit this route</a>
|
||||
<a class="btn btn-outline-secondary" href="admin.php">Add a route</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-card">
|
||||
<span class="eyebrow">More in <?= htmlspecialchars($route['city']) ?></span>
|
||||
|
||||
@ -282,6 +282,33 @@ function urban_hikes_find(int $id): ?array
|
||||
return $route ?: null;
|
||||
}
|
||||
|
||||
function urban_hikes_find_by_city(string $city): array
|
||||
{
|
||||
$storage = urban_hikes_storage();
|
||||
if (!$storage['ready'] || $city === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$stmt = $storage['pdo']->prepare('SELECT * FROM urban_routes WHERE city = :city ORDER BY distance_km ASC, title ASC');
|
||||
$stmt->execute([':city' => $city]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function urban_hikes_city_stats(string $city): ?array
|
||||
{
|
||||
$storage = urban_hikes_storage();
|
||||
if (!$storage['ready'] || $city === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sql = 'SELECT city, COUNT(*) AS route_count, AVG(distance_km) AS avg_distance, AVG(duration_hours) AS avg_duration, COUNT(DISTINCT neighborhood) AS area_count, SUM(CASE WHEN difficulty = "Easy" THEN 1 ELSE 0 END) AS easy_count, SUM(CASE WHEN difficulty = "Moderate" THEN 1 ELSE 0 END) AS moderate_count, SUM(CASE WHEN difficulty = "Challenging" THEN 1 ELSE 0 END) AS challenging_count FROM urban_routes WHERE city = :city GROUP BY city LIMIT 1';
|
||||
$stmt = $storage['pdo']->prepare($sql);
|
||||
$stmt->execute([':city' => $city]);
|
||||
$stats = $stmt->fetch();
|
||||
|
||||
return $stats ?: null;
|
||||
}
|
||||
|
||||
function urban_hikes_related(string $city, int $excludeId, int $limit = 3): array
|
||||
{
|
||||
$storage = urban_hikes_storage();
|
||||
@ -393,6 +420,45 @@ function urban_hikes_create(array $input): array
|
||||
return ['success' => true, 'id' => (int)$storage['pdo']->lastInsertId()];
|
||||
}
|
||||
|
||||
function urban_hikes_update(int $id, array $input): array
|
||||
{
|
||||
$storage = urban_hikes_storage();
|
||||
[$clean, $errors] = urban_hikes_validation($input);
|
||||
|
||||
if (!$storage['ready']) {
|
||||
return ['success' => false, 'errors' => ['storage' => 'Database is currently unavailable.'], 'input' => $clean];
|
||||
}
|
||||
|
||||
$existing = urban_hikes_find($id);
|
||||
if (!$existing) {
|
||||
return ['success' => false, 'errors' => ['storage' => 'That route could not be found.'], 'input' => $clean];
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
return ['success' => false, 'errors' => $errors, 'input' => $clean];
|
||||
}
|
||||
|
||||
$slug = $existing['title'] === $clean['title'] ? (string)$existing['slug'] : urban_hikes_unique_slug($storage['pdo'], $clean['title']);
|
||||
$stmt = $storage['pdo']->prepare('UPDATE urban_routes SET city = :city, title = :title, slug = :slug, summary = :summary, distance_km = :distance_km, duration_hours = :duration_hours, difficulty = :difficulty, neighborhood = :neighborhood, start_point = :start_point, highlights = :highlights, map_url = :map_url, best_for = :best_for WHERE id = :id LIMIT 1');
|
||||
$stmt->execute([
|
||||
':city' => $clean['city'],
|
||||
':title' => $clean['title'],
|
||||
':slug' => $slug,
|
||||
':summary' => $clean['summary'],
|
||||
':distance_km' => round((float)$clean['distance_km'], 1),
|
||||
':duration_hours' => round((float)$clean['duration_hours'], 1),
|
||||
':difficulty' => $clean['difficulty'],
|
||||
':neighborhood' => $clean['neighborhood'],
|
||||
':start_point' => $clean['start_point'],
|
||||
':highlights' => $clean['highlights'],
|
||||
':map_url' => $clean['map_url'],
|
||||
':best_for' => $clean['best_for'],
|
||||
':id' => $id,
|
||||
]);
|
||||
|
||||
return ['success' => true, 'id' => $id];
|
||||
}
|
||||
|
||||
function urban_hikes_highlight_items(string $value): array
|
||||
{
|
||||
$items = preg_split('/\r\n|\r|\n/', $value) ?: [];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user