mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:41:08 +02:00
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:
parent
1c5c2446f6
commit
512cf412cc
3 changed files with 81 additions and 3 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue