Auto commit: 2026-04-15T16:05:12.532Z

This commit is contained in:
Flatlogic Bot 2026-04-15 16:05:12 +00:00
parent 72bfa75c68
commit 1c7765e1cd
5 changed files with 466 additions and 142 deletions

340
admin.php
View File

@ -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
View 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(); ?>

View File

@ -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>

View File

@ -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>

View File

@ -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) ?: [];