214 lines
11 KiB
PHP
214 lines
11 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
require_once __DIR__ . '/urban_hikes.php';
|
||
|
||
$filters = urban_hikes_fetch_filters();
|
||
$routes = urban_hikes_search($filters);
|
||
$cities = urban_hikes_cities();
|
||
$stats = urban_hikes_stats();
|
||
$flash = urban_hikes_get_flash();
|
||
$storage = urban_hikes_storage();
|
||
|
||
$pageTitle = urban_hikes_project_name() . ' | Urban hiking routes directory';
|
||
$pageDescription = 'Find urban hiking routes in major cities by distance, time, and difficulty. Compare highlights, browse by city, and plan a full day on foot.';
|
||
|
||
urban_hikes_render_head($pageTitle, $pageDescription);
|
||
urban_hikes_render_nav('directory');
|
||
?>
|
||
<main>
|
||
<section class="hero-shell border-bottom">
|
||
<div class="container-lg px-3 px-lg-4 py-5 py-lg-6">
|
||
<div class="row g-4 align-items-end">
|
||
<div class="col-lg-7">
|
||
<span class="eyebrow">Urban hiking directory</span>
|
||
<h1 class="display-title mt-3 mb-3">Big-city routes for locals, visitors, and long walking days.</h1>
|
||
<p class="lead-copy mb-4">Search curated city hikes by place, distance, and difficulty. Every route gives you the basics fast: how long it takes, where it starts, and what makes it worth the walk.</p>
|
||
<div class="d-flex flex-wrap gap-2">
|
||
<a class="btn btn-dark px-4" href="#results">Browse routes</a>
|
||
<a class="btn btn-outline-secondary px-4" href="admin.php">Add a route</a>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-5">
|
||
<div class="panel-card stats-panel h-100">
|
||
<div class="row g-3">
|
||
<div class="col-4">
|
||
<p class="metric-label">Routes</p>
|
||
<p class="metric-value"><?= (int)$stats['routes'] ?></p>
|
||
</div>
|
||
<div class="col-4">
|
||
<p class="metric-label">Cities</p>
|
||
<p class="metric-value"><?= (int)$stats['cities'] ?></p>
|
||
</div>
|
||
<div class="col-4">
|
||
<p class="metric-label">Avg km</p>
|
||
<p class="metric-value"><?= htmlspecialchars(number_format((float)$stats['avg_distance'], 1)) ?></p>
|
||
</div>
|
||
</div>
|
||
<div class="divider my-4"></div>
|
||
<p class="small text-muted mb-2">Useful when you want:</p>
|
||
<ul class="compact-list mb-0">
|
||
<li>A scenic 2–3 hour city plan</li>
|
||
<li>A route that matches your walking energy</li>
|
||
<li>Quick route comparison before you leave the hotel</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-shell border-bottom" id="results">
|
||
<div class="container-lg px-3 px-lg-4 py-4 py-lg-5">
|
||
<?php if ($flash): ?>
|
||
<div class="toast-container position-fixed top-0 end-0 p-3">
|
||
<div class="toast app-toast text-bg-dark border-0" id="appToast" role="status" aria-live="polite" aria-atomic="true">
|
||
<div class="d-flex">
|
||
<div class="toast-body"><?= htmlspecialchars($flash['message']) ?></div>
|
||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!$storage['ready']): ?>
|
||
<div class="alert alert-warning mb-4" role="alert">
|
||
Route browsing is visible, but saving new routes is temporarily unavailable because the database connection failed.
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="row g-4 align-items-start">
|
||
<div class="col-xl-4">
|
||
<div class="panel-card sticky-filter-card">
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<div>
|
||
<span class="eyebrow">Find a route</span>
|
||
<h2 class="section-title mb-0 mt-2">Search filters</h2>
|
||
</div>
|
||
<a class="small text-decoration-none" href="index.php#results">Reset</a>
|
||
</div>
|
||
<form method="get" action="index.php#results" class="row g-3" data-results-form>
|
||
<div class="col-12">
|
||
<label class="form-label" for="q">Keyword</label>
|
||
<input class="form-control" type="text" id="q" name="q" value="<?= htmlspecialchars($filters['q']) ?>" placeholder="Canal, skyline, park, stairs" />
|
||
</div>
|
||
<div class="col-md-6 col-xl-12">
|
||
<label class="form-label" for="city">City</label>
|
||
<select class="form-select" id="city" name="city" data-autosubmit="change">
|
||
<option value="">All cities</option>
|
||
<?php foreach ($cities as $cityRow): ?>
|
||
<option value="<?= htmlspecialchars($cityRow['city']) ?>" <?= $filters['city'] === $cityRow['city'] ? 'selected' : '' ?>><?= htmlspecialchars($cityRow['city']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-6 col-xl-12">
|
||
<label class="form-label" for="difficulty">Difficulty</label>
|
||
<select class="form-select" id="difficulty" name="difficulty" data-autosubmit="change">
|
||
<option value="">Any level</option>
|
||
<?php foreach (['Easy', 'Moderate', 'Challenging'] as $level): ?>
|
||
<option value="<?= htmlspecialchars($level) ?>" <?= $filters['difficulty'] === $level ? 'selected' : '' ?>><?= htmlspecialchars($level) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label" for="max_distance">Maximum distance</label>
|
||
<select class="form-select" id="max_distance" name="max_distance" data-autosubmit="change">
|
||
<option value="">Any length</option>
|
||
<?php foreach ([5, 8, 10, 12] as $distance): ?>
|
||
<option value="<?= $distance ?>" <?= (float)$filters['max_distance'] === (float)$distance ? 'selected' : '' ?>>Up to <?= $distance ?> km</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="col-12 d-flex gap-2">
|
||
<button class="btn btn-dark flex-grow-1" type="submit">Apply filters</button>
|
||
<a class="btn btn-outline-secondary" href="index.php#results">Clear</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-xl-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">Directory</span>
|
||
<h2 class="section-title mb-1 mt-2"><?= count($routes) ?> route<?= count($routes) === 1 ? '' : 's' ?> matched</h2>
|
||
<p class="text-muted mb-0">Compact route cards with the essentials first: timing, effort, area, and highlights.</p>
|
||
</div>
|
||
<a class="btn btn-outline-secondary btn-sm align-self-start align-self-md-auto" href="admin.php">Suggest a new route</a>
|
||
</div>
|
||
|
||
<?php if (!$routes): ?>
|
||
<div class="panel-card empty-state">
|
||
<span class="eyebrow">No results</span>
|
||
<h3 class="h5 mt-2">No routes match these filters yet.</h3>
|
||
<p class="text-muted mb-4">Try widening the distance or removing the city filter. You can also add your own route to start a new city collection.</p>
|
||
<a class="btn btn-dark" href="admin.php">Add a route</a>
|
||
</div>
|
||
<?php else: ?>
|
||
<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-reverse flex-lg-row-reverse gap-3 justify-content-between">
|
||
<div class="route-image-placeholder" style="width: 200px; height: 150px; background: #deded8; border-radius: var(--radius-md);"></div>
|
||
<div class="flex-grow-1">
|
||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||
<span class="badge text-bg-light border"><?= htmlspecialchars($route['city']) ?></span>
|
||
<span class="badge text-bg-light border"><?= htmlspecialchars($route['difficulty']) ?></span>
|
||
<span class="badge text-bg-light border"><?= htmlspecialchars($route['neighborhood']) ?></span>
|
||
</div>
|
||
<h3 class="route-title mb-2"><a href="route.php?id=<?= (int)$route['id'] ?>"><?= htmlspecialchars($route['title']) ?></a></h3>
|
||
<p class="text-muted mb-3"><?= htmlspecialchars($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>Start: <?= htmlspecialchars($route['start_point']) ?></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="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>
|
||
</article>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-shell" id="cities">
|
||
<div class="container-lg px-3 px-lg-4 py-4 py-lg-5">
|
||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-end gap-3 mb-3">
|
||
<div>
|
||
<span class="eyebrow">Cities</span>
|
||
<h2 class="section-title mb-1 mt-2">Start from a city, then narrow it down.</h2>
|
||
<p class="text-muted mb-0">Fast jumping-off points for the places already in the directory.</p>
|
||
</div>
|
||
</div>
|
||
<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="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>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
<?php urban_hikes_render_footer(); ?>
|