From 512cf412cca7b08c1533082e437f7910d6bc08ea Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 23 Mar 2026 11:00:10 +0100 Subject: [PATCH] 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) --- .../src/location/location.controller.ts | 9 +++ .../backend/src/location/location.service.ts | 16 ++++- .../apps/web/src/routes/(app)/+layout.svelte | 59 ++++++++++++++++++- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/apps/citycorners/apps/backend/src/location/location.controller.ts b/apps/citycorners/apps/backend/src/location/location.controller.ts index 59b759e9a..768e06c70 100644 --- a/apps/citycorners/apps/backend/src/location/location.controller.ts +++ b/apps/citycorners/apps/backend/src/location/location.controller.ts @@ -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); diff --git a/apps/citycorners/apps/backend/src/location/location.service.ts b/apps/citycorners/apps/backend/src/location/location.service.ts index 05c6abf09..47050fb17 100644 --- a/apps/citycorners/apps/backend/src/location/location.service.ts +++ b/apps/citycorners/apps/backend/src/location/location.service.ts @@ -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 { + 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 { const [location] = await this.db.select().from(locations).where(eq(locations.id, id)); if (!location) { diff --git a/apps/citycorners/apps/web/src/routes/(app)/+layout.svelte b/apps/citycorners/apps/web/src/routes/(app)/+layout.svelte index 2c1ab4133..33b811503 100644 --- a/apps/citycorners/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/citycorners/apps/web/src/routes/(app)/+layout.svelte @@ -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 = { + 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 { + 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} + + +