Files
Critters/index.php
T
pauljd 35d187b4c2
pauljd/davidson-ci: Davidson CI / quality (push) Successful in 13s
pauljd/davidson-ci: Davidson CI / ai-review (push) Successful in 1m0s
further Davidson CI testing
2026-09-09 22:55:59 +01:00

511 lines
31 KiB
PHP

<?php
require __DIR__ . '/session.php';
require __DIR__ . '/db.php';
start_app_session();
$pdo = getDbConnection();
enforce_session_version($pdo);
$tableExists = $pdo->query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'adjectives'")->fetch();
if (!$tableExists) {
$pdo->exec('CREATE TABLE adjectives (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value TEXT NOT NULL
)');
$stmt = $pdo->prepare('INSERT INTO adjectives (value) VALUES (:value)');
foreach (['black', 'blue', 'brown', 'crochet', 'cute', 'fluffy', 'fuzzy', 'glittery', 'glowing neon', 'gray', 'green', 'lime green', 'magenta', 'orange', 'pastel', 'pink', 'purple', 'rainbow', 'red', 'sleepy', 'soft', 'sparkly', 'stone', 'teal', 'white', 'wooden', 'yellow'] as $adjective) {
$stmt->execute(['value' => $adjective]);
}
}
$tableExists = $pdo->query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'nouns'")->fetch();
if (!$tableExists) {
$pdo->exec('CREATE TABLE nouns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value TEXT NOT NULL
)');
$stmt = $pdo->prepare('INSERT INTO nouns (value) VALUES (:value)');
foreach (['beaver', 'bird', 'bunny rabbit', 'cat', 'chipmunk', 'cow', 'elephant', 'fox', 'frog', 'hamster', 'horse', 'kangaroo', 'koala', 'lemur', 'lion', 'monkey', 'moose', 'mouse', 'panda', 'penguin', 'pig', 'pika', 'puppy', 'quokka', 'raccoon', 'rat', 'sea otter', 'sheep', 'skunk', 'sloth', 'squirrel', 'teddy bear', 'tiger', 'turtle', 'wombat'] as $noun) {
$stmt->execute(['value' => $noun]);
}
}
$tableExists = $pdo->query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'verbs'")->fetch();
if (!$tableExists) {
$pdo->exec('CREATE TABLE verbs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value TEXT NOT NULL
)');
$stmt = $pdo->prepare('INSERT INTO verbs (value) VALUES (:value)');
foreach (['runs', 'jumps', 'waves hello', 'snuggles their blanket', 'swims', 'takes a bubble bath', 'wears a chef hat and stirs a baking bowl', 'hula hoops', 'builds with blocks', 'flies a kite', 'skateboards', 'rides their scooter', 'plays chess', 'writes with a pen and pad', 'plays guitar', 'plays the drums', 'plays the piano', 'roller skates', 'plays the saxophone', 'plays basketball', 'plays baseball', 'plays hockey', 'plays soccer', 'reads a book', 'wears pajamas', 'sleeps in bed', 'sings into a microphone', 'jumps rope', 'plays with a yo-yo', 'waters a plant with a watering can', 'sits by a campfire', 'naps in a hammock', 'plays golf', 'plays tennis', 'bakes cookies', 'does yoga'] as $verb) {
$stmt->execute(['value' => $verb]);
}
}
$tableExists = $pdo->query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'locations'")->fetch();
if (!$tableExists) {
$pdo->exec('CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value TEXT NOT NULL
)');
$stmt = $pdo->prepare('INSERT INTO locations (value) VALUES (:value)');
foreach (['at the beach', 'on a sunny island', 'in the backyard', 'inside a snow globe', 'inside a Christmas ornament', 'safely inside a hot air balloon up in the sky', 'on the moon', 'in front of a gnomes house', 'in a majestic waterfall', 'on a trampoline', 'inside an igloo', 'on a rainbow', 'underwater inside an aquarium', 'at the pumpkin patch', 'at the north pole', 'in a candy land', 'on a farm', 'in the frontyard', 'in a big grassy field', 'on top of a cloud', 'on a bridge', 'in a forest', 'at the top of a mountain', 'in a boat on a river', 'at the mall', 'on a baseball field', 'on a soccer field', 'on a basketball court', 'on a hockey rink', 'in a swimming pool', 'in a hot tub', 'in the kitchen'] as $location) {
$stmt->execute(['value' => $location]);
}
}
$isLoggedIn = isset($_SESSION['username']) && isset($_SESSION['user_id']);
if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$postedCritterId = $_POST['critter_id'] ?? '';
if (
csrf_verify($_POST['csrf_token'] ?? null)
&& in_array($action, ['toggle_favorite', 'toggle_archived'], true)
&& ctype_digit((string) $postedCritterId)
) {
$stmt = $pdo->prepare('SELECT author_id, isFavorite, isArchived FROM critters WHERE id = :id');
$stmt->execute(['id' => $postedCritterId]);
$target = $stmt->fetch(PDO::FETCH_ASSOC);
if ($target && (int) $target['author_id'] === (int) $_SESSION['user_id']) {
if ($action === 'toggle_favorite') {
$newValue = $target['isFavorite'] ? 0 : 1;
$update = $pdo->prepare('UPDATE critters SET isFavorite = :value WHERE id = :id');
} else {
$newValue = $target['isArchived'] ? 0 : 1;
$update = $pdo->prepare('UPDATE critters SET isArchived = :value WHERE id = :id');
}
$update->execute(['value' => $newValue, 'id' => $postedCritterId]);
}
}
$redirectQuery = $_SERVER['QUERY_STRING'] ?? '';
header('Location: /' . ($redirectQuery !== '' ? '?' . $redirectQuery : ''));
exit;
}
function renderOptions(array $rows, $selectedId): void
{
foreach ($rows as $row) {
$isSelected = (string) $selectedId === (string) $row['id'];
echo '<option value="' . (int) $row['id'] . '"' . ($isSelected ? ' selected' : '') . '>'
. htmlspecialchars($row['value'], ENT_QUOTES, 'UTF-8')
. '</option>';
}
}
function buildCritterUrl(array $overrides, array $filters, string $statusFilter, string $search, string $sort, int $page): string
{
$params = [];
foreach ($filters as $key => $value) {
if ($value !== '') {
$params[$key] = $value;
}
}
if ($statusFilter !== 'any') {
$params['status'] = $statusFilter;
}
if ($search !== '') {
$params['q'] = $search;
}
if ($sort !== 'newest') {
$params['sort'] = $sort;
}
$params['page'] = $page;
foreach ($overrides as $key => $value) {
if ($value === null) {
unset($params[$key]);
} else {
$params[$key] = $value;
}
}
return '?' . http_build_query($params);
}
$myCritters = [];
$adjectives = [];
$nouns = [];
$verbs = [];
$locations = [];
$filters = [
'filter_adjective' => '',
'filter_noun' => '',
'filter_verb' => '',
'filter_location' => '',
];
$statusFilter = 'any';
$search = '';
$sort = 'newest';
$perPage = 8;
$page = 1;
$totalPages = 1;
$totalCrittersCount = 0;
$favoritesCount = 0;
$archivedCount = 0;
if ($isLoggedIn) {
$adjectives = $pdo->query('SELECT id, value FROM adjectives ORDER BY value')->fetchAll(PDO::FETCH_ASSOC);
$nouns = $pdo->query('SELECT id, value FROM nouns ORDER BY value')->fetchAll(PDO::FETCH_ASSOC);
$verbs = $pdo->query('SELECT id, value FROM verbs ORDER BY value')->fetchAll(PDO::FETCH_ASSOC);
$locations = $pdo->query('SELECT id, value FROM locations ORDER BY value')->fetchAll(PDO::FETCH_ASSOC);
$filterFields = [
'filter_adjective' => ['column' => 'adjective_id', 'table' => 'adjectives'],
'filter_noun' => ['column' => 'noun_id', 'table' => 'nouns'],
'filter_verb' => ['column' => 'verb_id', 'table' => 'verbs'],
'filter_location' => ['column' => 'location_id', 'table' => 'locations'],
];
foreach ($filterFields as $field => $info) {
$value = $_GET[$field] ?? '';
if ($value !== '' && ctype_digit((string) $value) && rowExistsById($pdo, $info['table'], $value)) {
$filters[$field] = (int) $value;
}
}
$allowedStatuses = ['any', 'favorited', 'archived'];
$requestedStatus = $_GET['status'] ?? 'any';
if (in_array($requestedStatus, $allowedStatuses, true)) {
$statusFilter = $requestedStatus;
}
$search = trim((string) ($_GET['q'] ?? ''));
if (strlen($search) > 100) {
$search = substr($search, 0, 100);
}
$requestedSort = $_GET['sort'] ?? 'newest';
if (in_array($requestedSort, ['newest', 'oldest'], true)) {
$sort = $requestedSort;
}
$requestedPage = $_GET['page'] ?? '1';
if (ctype_digit((string) $requestedPage) && (int) $requestedPage >= 1) {
$page = (int) $requestedPage;
}
$crittersTableExists = $pdo->query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'critters'")->fetch();
if ($crittersTableExists) {
$countAllStmt = $pdo->prepare('SELECT COUNT(*) FROM critters WHERE author_id = :author_id');
$countAllStmt->execute(['author_id' => $_SESSION['user_id']]);
$totalCrittersCount = (int) $countAllStmt->fetchColumn();
$countFavStmt = $pdo->prepare('SELECT COUNT(*) FROM critters WHERE author_id = :author_id AND isFavorite = 1');
$countFavStmt->execute(['author_id' => $_SESSION['user_id']]);
$favoritesCount = (int) $countFavStmt->fetchColumn();
$countArcStmt = $pdo->prepare('SELECT COUNT(*) FROM critters WHERE author_id = :author_id AND isArchived = 1');
$countArcStmt->execute(['author_id' => $_SESSION['user_id']]);
$archivedCount = (int) $countArcStmt->fetchColumn();
$whereConditions = ['c.author_id = :author_id'];
$queryParams = ['author_id' => $_SESSION['user_id']];
foreach ($filters as $field => $value) {
if ($value !== '') {
$column = $filterFields[$field]['column'];
$whereConditions[] = "c.{$column} = :{$field}";
$queryParams[$field] = $value;
}
}
if ($statusFilter === 'favorited') {
$whereConditions[] = 'c.isFavorite = 1';
} elseif ($statusFilter === 'archived') {
$whereConditions[] = 'c.isArchived = 1';
}
if ($search !== '') {
$escapedSearch = addcslashes($search, '%_\\');
$whereConditions[] = "(a.value LIKE :search ESCAPE '\\' OR n.value LIKE :search ESCAPE '\\' OR v.value LIKE :search ESCAPE '\\' OR l.value LIKE :search ESCAPE '\\')";
$queryParams['search'] = '%' . $escapedSearch . '%';
}
$whereSql = implode(' AND ', $whereConditions);
$countStmt = $pdo->prepare("
SELECT COUNT(*)
FROM critters c
JOIN adjectives a ON a.id = c.adjective_id
JOIN nouns n ON n.id = c.noun_id
JOIN verbs v ON v.id = c.verb_id
JOIN locations l ON l.id = c.location_id
WHERE {$whereSql}
");
$countStmt->execute($queryParams);
$totalCritters = (int) $countStmt->fetchColumn();
$totalPages = max(1, (int) ceil($totalCritters / $perPage));
if ($page > $totalPages) {
$page = $totalPages;
}
$offset = ($page - 1) * $perPage;
$orderSql = $sort === 'oldest' ? 'c.id ASC' : 'c.id DESC';
$sql = "
SELECT c.id AS id, c.isFavorite AS isFavorite, c.isArchived AS isArchived,
a.value AS adjective, n.value AS noun, v.value AS verb, l.value AS location, c.image AS image
FROM critters c
JOIN adjectives a ON a.id = c.adjective_id
JOIN nouns n ON n.id = c.noun_id
JOIN verbs v ON v.id = c.verb_id
JOIN locations l ON l.id = c.location_id
WHERE {$whereSql}
ORDER BY {$orderSql}
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $pdo->prepare($sql);
$stmt->execute($queryParams);
$myCritters = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
$pageTitle = 'Welcome';
?>
<!DOCTYPE html>
<html lang="en">
<?php require __DIR__ . '/lib/head.php'; ?>
<body class="<?php echo $isLoggedIn ? 'bg-amber-50' : ''; ?> min-h-screen flex flex-col">
<main class="flex-1">
<?php if ($isLoggedIn): ?>
<?php require __DIR__ . '/lib/header.php'; ?>
<?php require __DIR__ . '/lib/critters-hero.php'; ?>
<div class="max-w-[1400px] mx-auto px-6 mt-8">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div class="flex flex-wrap items-center gap-2">
<a href="<?php echo htmlspecialchars(buildCritterUrl(['status' => null, 'page' => 1], $filters, $statusFilter, $search, $sort, $page), ENT_QUOTES, 'UTF-8'); ?>"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 <?php echo $statusFilter === 'any' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'; ?>">
<i data-lucide="layout-grid" class="h-4 w-4"></i> All Critters
<span class="inline-flex items-center justify-center h-5 min-w-5 px-1 rounded-full bg-white/70 text-xs font-semibold"><?php echo $totalCrittersCount; ?></span>
</a>
<a href="<?php echo htmlspecialchars(buildCritterUrl(['status' => 'favorited', 'page' => 1], $filters, $statusFilter, $search, $sort, $page), ENT_QUOTES, 'UTF-8'); ?>"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 <?php echo $statusFilter === 'favorited' ? 'bg-pink-100 text-pink-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'; ?>">
<i data-lucide="heart" class="h-4 w-4"></i> Favourites
<span class="inline-flex items-center justify-center h-5 min-w-5 px-1 rounded-full bg-white/70 text-xs font-semibold"><?php echo $favoritesCount; ?></span>
</a>
<a href="<?php echo htmlspecialchars(buildCritterUrl(['status' => 'archived', 'page' => 1], $filters, $statusFilter, $search, $sort, $page), ENT_QUOTES, 'UTF-8'); ?>"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 <?php echo $statusFilter === 'archived' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'; ?>">
<i data-lucide="archive" class="h-4 w-4"></i> Archived
<span class="inline-flex items-center justify-center h-5 min-w-5 px-1 rounded-full bg-white/70 text-xs font-semibold"><?php echo $archivedCount; ?></span>
</a>
</div>
<form action="" method="get" class="flex flex-wrap items-center gap-3">
<input type="hidden" name="filter_adjective" value="<?php echo htmlspecialchars((string) $filters['filter_adjective'], ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="filter_noun" value="<?php echo htmlspecialchars((string) $filters['filter_noun'], ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="filter_verb" value="<?php echo htmlspecialchars((string) $filters['filter_verb'], ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="filter_location" value="<?php echo htmlspecialchars((string) $filters['filter_location'], ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="status" value="<?php echo htmlspecialchars($statusFilter, ENT_QUOTES, 'UTF-8'); ?>">
<div class="relative">
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400"></i>
<input
type="text" name="q" value="<?php echo htmlspecialchars($search, ENT_QUOTES, 'UTF-8'); ?>"
placeholder="Search your critters..."
class="rounded-xl border border-gray-300 pl-9 pr-3 py-2 text-sm w-56 transition-shadow focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent"
>
</div>
<label class="flex items-center gap-2 text-sm text-gray-600">
Sort by
<select name="sort" onchange="this.form.submit()" class="rounded-xl border border-gray-300 px-3 py-2 text-sm transition-shadow focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent">
<option value="newest" <?php echo $sort === 'newest' ? 'selected' : ''; ?>>Newest</option>
<option value="oldest" <?php echo $sort === 'oldest' ? 'selected' : ''; ?>>Oldest</option>
</select>
</label>
<button type="submit" class="sr-only">Search</button>
</form>
</div>
<?php if (empty($myCritters)): ?>
<div class="mt-10 flex flex-col items-center text-center py-12">
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-purple-50 text-purple-300 mb-4">
<i data-lucide="paw-print" class="h-8 w-8"></i>
</div>
<p class="text-gray-500">
<?php echo $totalCrittersCount === 0 ? "You haven't created any critters yet." : 'No critters match your current filters.'; ?>
</p>
<?php if ($totalCrittersCount === 0): ?>
<a href="create-critter.php" class="mt-4 inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-purple-600 to-pink-500 text-white text-sm font-semibold px-5 py-2.5 shadow-sm transition-all duration-200 hover:from-purple-700 hover:to-pink-600 hover:shadow-md active:scale-[0.98]">
<i data-lucide="plus" class="h-4 w-4"></i> Create your first critter
</a>
<?php endif; ?>
</div>
<?php else: ?>
<div class="mt-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6">
<?php foreach ($myCritters as $critter): ?>
<?php if (empty($critter['image'])) continue; ?>
<div class="group rounded-2xl bg-white ring-1 ring-gray-100 shadow-sm overflow-hidden transition-all duration-200 hover:shadow-lg hover:-translate-y-1" data-critter-card="<?php echo (int) $critter['id']; ?>">
<div class="relative overflow-hidden">
<button type="button" onclick="openCritterModal(<?php echo (int) $critter['id']; ?>)" class="block w-full focus:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 focus-visible:ring-inset">
<img
src="critter-images/<?php echo htmlspecialchars($critter['image'], ENT_QUOTES, 'UTF-8'); ?>"
alt="AI-generated image of your critter"
class="w-full aspect-square object-cover transition-transform duration-300 group-hover:scale-105"
>
</button>
<form action="" method="post" class="absolute top-3 right-3">
<?php echo csrf_field(); ?>
<input type="hidden" name="action" value="toggle_favorite">
<input type="hidden" name="critter_id" value="<?php echo (int) $critter['id']; ?>">
<button type="submit" aria-label="<?php echo $critter['isFavorite'] ? 'Unfavorite' : 'Favorite'; ?>" class="flex h-8 w-8 items-center justify-center rounded-full bg-white/90 shadow-sm transition-transform duration-150 hover:bg-white hover:scale-110">
<i data-lucide="heart" data-favorite-icon class="h-4 w-4 <?php echo $critter['isFavorite'] ? 'text-pink-500 fill-pink-500' : 'text-gray-400'; ?>"></i>
</button>
</form>
</div>
<div class="p-4">
<div class="flex items-center justify-between gap-2">
<h3 class="font-semibold text-gray-900 truncate"><?php echo htmlspecialchars(ucwords($critter['adjective'] . ' ' . $critter['noun']), ENT_QUOTES, 'UTF-8'); ?></h3>
<span class="shrink-0 text-xs text-gray-400">#<?php echo str_pad((string) $critter['id'], 4, '0', STR_PAD_LEFT); ?></span>
</div>
<div class="mt-2 flex flex-wrap gap-1.5">
<span class="inline-flex items-center rounded-full bg-blue-50 text-blue-700 text-xs font-medium px-2 py-1 truncate max-w-full"><?php echo htmlspecialchars(ucfirst($critter['verb']), ENT_QUOTES, 'UTF-8'); ?></span>
<span class="inline-flex items-center rounded-full bg-green-50 text-green-700 text-xs font-medium px-2 py-1 truncate max-w-full"><?php echo htmlspecialchars(ucfirst($critter['location']), ENT_QUOTES, 'UTF-8'); ?></span>
</div>
<div class="mt-3 flex items-center gap-2 border-t border-gray-100 pt-3">
<button type="button" onclick="openCritterModal(<?php echo (int) $critter['id']; ?>)" aria-label="View critter" class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 transition-colors duration-150 hover:bg-gray-100 hover:text-purple-600">
<i data-lucide="eye" class="h-4 w-4"></i>
</button>
<form action="" method="post">
<?php echo csrf_field(); ?>
<input type="hidden" name="action" value="toggle_favorite">
<input type="hidden" name="critter_id" value="<?php echo (int) $critter['id']; ?>">
<button type="submit" aria-label="<?php echo $critter['isFavorite'] ? 'Unfavorite' : 'Favorite'; ?>" class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 transition-colors duration-150 hover:bg-gray-100 hover:text-pink-500">
<i data-lucide="heart" data-favorite-icon class="h-4 w-4 <?php echo $critter['isFavorite'] ? 'text-pink-500 fill-pink-500' : ''; ?>"></i>
</button>
</form>
<form action="" method="post">
<?php echo csrf_field(); ?>
<input type="hidden" name="action" value="toggle_archived">
<input type="hidden" name="critter_id" value="<?php echo (int) $critter['id']; ?>">
<button type="submit" aria-label="<?php echo $critter['isArchived'] ? 'Unarchive' : 'Archive'; ?>" class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 transition-colors duration-150 hover:bg-gray-100 hover:text-green-600">
<i data-lucide="<?php echo $critter['isArchived'] ? 'archive-restore' : 'archive'; ?>" class="h-4 w-4"></i>
</button>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
<?php if (count($myCritters) < $perPage): ?>
<div class="rounded-2xl border-2 border-dashed border-gray-200 flex flex-col items-center justify-center text-center p-6 transition-colors duration-200 hover:border-purple-200 hover:bg-purple-50/30">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-blue-50 text-blue-300 mb-3">
<i data-lucide="paw-print" class="h-6 w-6"></i>
</div>
<h3 class="font-semibold text-gray-900">Ready for another critter?</h3>
<p class="mt-1 text-sm text-gray-500">Use your imagination and create something amazing!</p>
<a href="create-critter.php" class="mt-4 inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-purple-600 to-pink-500 text-white text-sm font-semibold px-4 py-2 shadow-sm transition-all duration-200 hover:from-purple-700 hover:to-pink-600 hover:shadow-md active:scale-[0.98]">
<i data-lucide="plus" class="h-4 w-4"></i> Create a Critter
</a>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($totalPages > 1): ?>
<div class="mt-6 flex justify-center items-center gap-2">
<?php if ($page > 1): ?>
<a href="<?php echo htmlspecialchars(buildCritterUrl(['page' => $page - 1], $filters, $statusFilter, $search, $sort, $page), ENT_QUOTES, 'UTF-8'); ?>" class="px-3 py-2 rounded-md border border-gray-300 text-gray-700 transition-colors hover:bg-gray-100">Previous</a>
<?php else: ?>
<span class="px-3 py-2 rounded-md border border-gray-200 text-gray-400 cursor-not-allowed">Previous</span>
<?php endif; ?>
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<?php if ($i === $page): ?>
<span class="px-3 py-2 rounded-md bg-gray-900 text-white font-medium"><?php echo $i; ?></span>
<?php else: ?>
<a href="<?php echo htmlspecialchars(buildCritterUrl(['page' => $i], $filters, $statusFilter, $search, $sort, $page), ENT_QUOTES, 'UTF-8'); ?>" class="px-3 py-2 rounded-md border border-gray-300 text-gray-700 transition-colors hover:bg-gray-100"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor; ?>
<?php if ($page < $totalPages): ?>
<a href="<?php echo htmlspecialchars(buildCritterUrl(['page' => $page + 1], $filters, $statusFilter, $search, $sort, $page), ENT_QUOTES, 'UTF-8'); ?>" class="px-3 py-2 rounded-md border border-gray-300 text-gray-700 transition-colors hover:bg-gray-100">Next</a>
<?php else: ?>
<span class="px-3 py-2 rounded-md border border-gray-200 text-gray-400 cursor-not-allowed">Next</span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="mt-8 mb-12 rounded-2xl bg-blue-50/60 ring-1 ring-blue-100 p-6">
<div class="flex items-start gap-3 mb-4">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-100 text-blue-600">
<i data-lucide="funnel" class="h-5 w-5"></i>
</div>
<div>
<h2 class="font-semibold text-gray-900">Filter Your Critters</h2>
<p class="text-sm text-gray-500">Find exactly what you're looking for.</p>
</div>
</div>
<form action="" method="get" class="flex flex-wrap items-end gap-4">
<input type="hidden" name="q" value="<?php echo htmlspecialchars($search, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="sort" value="<?php echo htmlspecialchars($sort, ENT_QUOTES, 'UTF-8'); ?>">
<div>
<label for="filter_adjective" class="block text-sm font-medium text-gray-700 mb-1">Adjective</label>
<select id="filter_adjective" name="filter_adjective" class="rounded-md border border-gray-300 px-3 py-2 text-gray-900 focus:outline-none focus:ring-2 focus:ring-purple-400 transition-shadow">
<option value="">Any</option>
<?php renderOptions($adjectives, $filters['filter_adjective']); ?>
</select>
</div>
<div>
<label for="filter_noun" class="block text-sm font-medium text-gray-700 mb-1">Noun (Animal)</label>
<select id="filter_noun" name="filter_noun" class="rounded-md border border-gray-300 px-3 py-2 text-gray-900 focus:outline-none focus:ring-2 focus:ring-purple-400 transition-shadow">
<option value="">Any</option>
<?php renderOptions($nouns, $filters['filter_noun']); ?>
</select>
</div>
<div>
<label for="filter_verb" class="block text-sm font-medium text-gray-700 mb-1">Verb (Action)</label>
<select id="filter_verb" name="filter_verb" class="rounded-md border border-gray-300 px-3 py-2 text-gray-900 focus:outline-none focus:ring-2 focus:ring-purple-400 transition-shadow">
<option value="">Any</option>
<?php renderOptions($verbs, $filters['filter_verb']); ?>
</select>
</div>
<div>
<label for="filter_location" class="block text-sm font-medium text-gray-700 mb-1">Location</label>
<select id="filter_location" name="filter_location" class="rounded-md border border-gray-300 px-3 py-2 text-gray-900 focus:outline-none focus:ring-2 focus:ring-purple-400 transition-shadow">
<option value="">Any</option>
<?php renderOptions($locations, $filters['filter_location']); ?>
</select>
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select id="status" name="status" class="rounded-md border border-gray-300 px-3 py-2 text-gray-900 focus:outline-none focus:ring-2 focus:ring-purple-400 transition-shadow">
<option value="any" <?php echo $statusFilter === 'any' ? 'selected' : ''; ?>>Any</option>
<option value="favorited" <?php echo $statusFilter === 'favorited' ? 'selected' : ''; ?>>Favorited</option>
<option value="archived" <?php echo $statusFilter === 'archived' ? 'selected' : ''; ?>>Archived</option>
</select>
</div>
<div class="flex items-center gap-2">
<a href="<?php echo htmlspecialchars(buildCritterUrl(['filter_adjective' => null, 'filter_noun' => null, 'filter_verb' => null, 'filter_location' => null, 'status' => null, 'page' => 1], $filters, $statusFilter, $search, $sort, $page), ENT_QUOTES, 'UTF-8'); ?>" class="px-4 py-2 rounded-md border border-gray-300 text-gray-700 font-medium transition-colors hover:bg-gray-100">Clear</a>
<button type="submit" class="inline-flex items-center gap-2 bg-gray-900 text-white rounded-md px-4 py-2 font-medium transition-all duration-150 hover:bg-gray-700 active:scale-[0.98]">
<i data-lucide="search" class="h-4 w-4"></i> Apply Filters
</button>
</div>
</form>
</div>
</div>
<?php require __DIR__ . '/lib/critter-modal.php'; ?>
<?php else: ?>
<?php require __DIR__ . '/lib/hero.php'; ?>
<?php require __DIR__ . '/lib/how-it-works.php'; ?>
<?php require __DIR__ . '/lib/recent-critters.php'; ?>
<?php require __DIR__ . '/lib/cta.php'; ?>
<?php endif; ?>
</main>
<?php require __DIR__ . '/lib/footer.php'; ?>
</body>
</html>