feat(citycorners): add location search with QuickInputBar integration

Backend: GET /locations/search?q= endpoint with ILIKE on name, description, address.
Frontend: QuickInputBar wired up in app layout, searches locations via API, navigates to detail page on select.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-23 11:00:10 +01:00
parent 1c5c2446f6
commit 512cf412cc
3 changed files with 81 additions and 3 deletions

View file

@ -78,6 +78,15 @@ export class LocationController {
return { locations };
}
@Get('search')
async search(@Query('q') query: string) {
if (!query || query.trim().length === 0) {
return { locations: [] };
}
const locations = await this.locationService.search(query.trim());
return { locations };
}
@Get(':id')
async findById(@Param('id') id: string) {
const location = await this.locationService.findById(id);

View file

@ -1,5 +1,5 @@
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { eq, or, ilike } from 'drizzle-orm';
import { DATABASE_CONNECTION } from '../db/database.module';
import { Database } from '../db/connection';
import { locations } from '../db/schema';
@ -19,6 +19,20 @@ export class LocationService {
return this.db.select().from(locations);
}
async search(query: string): Promise<Location[]> {
const pattern = `%${query}%`;
return this.db
.select()
.from(locations)
.where(
or(
ilike(locations.name, pattern),
ilike(locations.description, pattern),
ilike(locations.address, pattern)
)
);
}
async findById(id: string): Promise<Location> {
const [location] = await this.db.select().from(locations).where(eq(locations.id, id));
if (!location) {

View file

@ -2,8 +2,8 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { favoritesStore } from '$lib/stores/favorites.svelte';
@ -57,6 +57,49 @@
goto('/login');
}
const categoryLabels: Record<string, string> = {
sight: 'Sehenswürdigkeit',
restaurant: 'Restaurant',
shop: 'Laden',
museum: 'Museum',
};
const backendUrl =
typeof window !== 'undefined'
? (window as any).__PUBLIC_BACKEND_URL__ || 'http://localhost:3025'
: 'http://localhost:3025';
let inputBarBottomOffset = $derived(showNav ? '70px' : '16px');
interface SearchItem extends QuickInputItem {
href?: string;
}
async function handleSearch(query: string): Promise<SearchItem[]> {
if (!query.trim()) return [];
try {
const res = await fetch(`${backendUrl}/locations/search?q=${encodeURIComponent(query)}`);
if (!res.ok) return [];
const data = await res.json();
return data.locations.slice(0, 8).map((loc: any) => ({
id: loc.id,
title: loc.name,
subtitle: categoryLabels[loc.category] || loc.category,
icon: 'mappin' as const,
href: `/locations/${loc.id}`,
}));
} catch {
return [];
}
}
function handleSelect(item: SearchItem) {
if (item.href) {
goto(item.href);
}
}
function handleNavToggle() {
showNav = !showNav;
}
@ -93,6 +136,18 @@
/>
{/if}
<!-- Quick Search Bar -->
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
placeholder="Ort suchen..."
emptyText="Keine Ergebnisse"
searchingText="Suche..."
appIcon="mappin"
bottomOffset={inputBarBottomOffset}
hasFabRight={true}
/>
<button
class="pillnav-fab"
onclick={handleNavToggle}