diff --git a/apps/citycorners/apps/web/src/lib/api.ts b/apps/citycorners/apps/web/src/lib/api.ts new file mode 100644 index 000000000..e87567601 --- /dev/null +++ b/apps/citycorners/apps/web/src/lib/api.ts @@ -0,0 +1,14 @@ +import { browser } from '$app/environment'; + +export function getBackendUrl(): string { + if (browser && typeof window !== 'undefined') { + const injectedUrl = (window as any).__PUBLIC_BACKEND_URL__; + if (injectedUrl) return injectedUrl; + return import.meta.env.DEV ? 'http://localhost:3025' : ''; + } + return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3025'; +} + +export function api(path: string): string { + return `${getBackendUrl()}/api/v1${path}`; +} diff --git a/apps/citycorners/apps/web/src/lib/i18n/locales/de.json b/apps/citycorners/apps/web/src/lib/i18n/locales/de.json index 332951f91..a441cd64d 100644 --- a/apps/citycorners/apps/web/src/lib/i18n/locales/de.json +++ b/apps/citycorners/apps/web/src/lib/i18n/locales/de.json @@ -6,6 +6,7 @@ "nav": { "explore": "Entdecken", "map": "Karte", + "add": "Hinzufügen", "favorites": "Favoriten", "settings": "Einstellungen", "showNav": "Navigation einblenden", @@ -74,6 +75,22 @@ "loginTitle": "Login - CityCorners", "registerTitle": "Registrieren - CityCorners" }, + "add": { + "title": "Ort hinzufügen", + "subtitle": "Teile deinen Lieblingsort in Konstanz", + "name": "Name", + "namePlaceholder": "z.B. Café am See", + "category": "Kategorie", + "description": "Beschreibung", + "descriptionPlaceholder": "Was macht diesen Ort besonders?", + "minChars": "Mindestens 10 Zeichen", + "address": "Adresse (optional)", + "addressPlaceholder": "z.B. Seestraße 1, 78462 Konstanz", + "submit": "Ort einreichen", + "submitting": "Wird eingereicht...", + "loginRequired": "Melde dich an, um Orte hinzuzufügen.", + "error": "Fehler beim Einreichen. Bitte versuche es erneut." + }, "offline": { "title": "Keine Verbindung", "message": "Du bist gerade offline. Sobald du wieder eine Internetverbindung hast, kannst du CityCorners weiter nutzen.", diff --git a/apps/citycorners/apps/web/src/lib/i18n/locales/en.json b/apps/citycorners/apps/web/src/lib/i18n/locales/en.json index f26d41d8d..157ec6e2a 100644 --- a/apps/citycorners/apps/web/src/lib/i18n/locales/en.json +++ b/apps/citycorners/apps/web/src/lib/i18n/locales/en.json @@ -6,6 +6,7 @@ "nav": { "explore": "Explore", "map": "Map", + "add": "Add", "favorites": "Favorites", "settings": "Settings", "showNav": "Show navigation", @@ -74,6 +75,22 @@ "loginTitle": "Login - CityCorners", "registerTitle": "Sign up - CityCorners" }, + "add": { + "title": "Add a place", + "subtitle": "Share your favorite spot in Konstanz", + "name": "Name", + "namePlaceholder": "e.g. Lakeside Café", + "category": "Category", + "description": "Description", + "descriptionPlaceholder": "What makes this place special?", + "minChars": "At least 10 characters", + "address": "Address (optional)", + "addressPlaceholder": "e.g. Seestraße 1, 78462 Konstanz", + "submit": "Submit place", + "submitting": "Submitting...", + "loginRequired": "Sign in to add places.", + "error": "Failed to submit. Please try again." + }, "offline": { "title": "No connection", "message": "You are currently offline. You can continue using CityCorners once you have an internet connection again.", diff --git a/apps/citycorners/apps/web/src/lib/stores/favorites.svelte.ts b/apps/citycorners/apps/web/src/lib/stores/favorites.svelte.ts index 17ebb68e2..466cd4b1a 100644 --- a/apps/citycorners/apps/web/src/lib/stores/favorites.svelte.ts +++ b/apps/citycorners/apps/web/src/lib/stores/favorites.svelte.ts @@ -2,8 +2,8 @@ * Favorites Store - Manages favorite locations using Svelte 5 runes */ -import { browser } from '$app/environment'; import { authStore } from './auth.svelte'; +import { api } from '$lib/api'; interface Favorite { id: string; @@ -12,13 +12,6 @@ interface Favorite { createdAt: string; } -function getBackendUrl(): string { - if (browser && typeof window !== 'undefined') { - return (window as any).__PUBLIC_BACKEND_URL__ || 'http://localhost:3025'; - } - return 'http://localhost:3025'; -} - let favoriteLocationIds = $state>(new Set()); let loading = $state(false); @@ -42,7 +35,7 @@ export const favoritesStore = { const token = await authStore.getValidToken(); if (!token) return; - const res = await fetch(`${getBackendUrl()}/favorites`, { + const res = await fetch(`${api('/favorites')}`, { headers: { Authorization: `Bearer ${token}` }, }); @@ -75,7 +68,7 @@ export const favoritesStore = { favoriteLocationIds = newSet; try { - const res = await fetch(`${getBackendUrl()}/favorites/${locationId}`, { + const res = await fetch(`${api(`/favorites/${locationId}`)}`, { method: isFav ? 'DELETE' : 'POST', headers: { Authorization: `Bearer ${token}` }, }); diff --git a/apps/citycorners/apps/web/src/routes/(app)/+layout.svelte b/apps/citycorners/apps/web/src/routes/(app)/+layout.svelte index 2dd8fa343..5c961f69d 100644 --- a/apps/citycorners/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/citycorners/apps/web/src/routes/(app)/+layout.svelte @@ -13,6 +13,7 @@ import { getPillAppItems } from '@manacore/shared-branding'; import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n'; import { setLocale, supportedLocales } from '$lib/i18n'; + import { api } from '$lib/api'; const appItems = getPillAppItems('citycorners'); @@ -52,6 +53,7 @@ let navItems = $derived([ { href: '/', label: $_('nav.explore'), icon: 'compass' }, { href: '/map', label: $_('nav.map'), icon: 'mappin' }, + { href: '/add', label: $_('nav.add'), icon: 'plus' }, { href: '/favorites', label: $_('nav.favorites'), icon: 'heart' }, { href: '/settings', label: $_('nav.settings'), icon: 'settings' }, ]); @@ -70,11 +72,6 @@ goto('/login'); } - 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 { @@ -85,7 +82,7 @@ if (!query.trim()) return []; try { - const res = await fetch(`${backendUrl}/locations/search?q=${encodeURIComponent(query)}`); + const res = await fetch(api(`/locations/search?q=${encodeURIComponent(query)}`)); if (!res.ok) return []; const data = await res.json(); return data.locations.slice(0, 8).map((loc: any) => ({ diff --git a/apps/citycorners/apps/web/src/routes/(app)/+page.svelte b/apps/citycorners/apps/web/src/routes/(app)/+page.svelte index 58060d426..b86655c89 100644 --- a/apps/citycorners/apps/web/src/routes/(app)/+page.svelte +++ b/apps/citycorners/apps/web/src/routes/(app)/+page.svelte @@ -3,6 +3,7 @@ import { _ } from 'svelte-i18n'; import { authStore } from '$lib/stores/auth.svelte'; import { favoritesStore } from '$lib/stores/favorites.svelte'; + import { api } from '$lib/api'; interface Location { id: string; @@ -19,11 +20,6 @@ let loading = $state(true); let selectedCategory = $state(null); - const backendUrl = - typeof window !== 'undefined' - ? (window as any).__PUBLIC_BACKEND_URL__ || 'http://localhost:3025' - : 'http://localhost:3025'; - const categoryKeys = ['sight', 'restaurant', 'shop', 'museum']; let filtered = $derived( @@ -32,7 +28,7 @@ onMount(async () => { try { - const res = await fetch(`${backendUrl}/locations`); + const res = await fetch(api('/locations')); const data = await res.json(); locations = data.locations; } catch (err) { diff --git a/apps/citycorners/apps/web/src/routes/(app)/add/+page.svelte b/apps/citycorners/apps/web/src/routes/(app)/add/+page.svelte new file mode 100644 index 000000000..c831fab49 --- /dev/null +++ b/apps/citycorners/apps/web/src/routes/(app)/add/+page.svelte @@ -0,0 +1,164 @@ + + + + {$_('add.title')} - CityCorners + + +
+

{$_('add.title')}

+

{$_('add.subtitle')}

+
+ +{#if !authStore.isAuthenticated} +
+ 📍 +

{$_('add.loginRequired')}

+ + {$_('settings.login')} + +
+{:else} +
{ + e.preventDefault(); + handleSubmit(); + }} + class="space-y-5" + > + {#if error} +
{error}
+ {/if} + +
+ + +
+ +
+ +
+ {#each categories as cat} + + {/each} +
+
+ +
+ + +

{$_('add.minChars')}

+
+ +
+ + +
+ + +
+{/if} diff --git a/apps/citycorners/apps/web/src/routes/(app)/favorites/+page.svelte b/apps/citycorners/apps/web/src/routes/(app)/favorites/+page.svelte index 411114c94..18a74ea6d 100644 --- a/apps/citycorners/apps/web/src/routes/(app)/favorites/+page.svelte +++ b/apps/citycorners/apps/web/src/routes/(app)/favorites/+page.svelte @@ -3,6 +3,7 @@ import { _ } from 'svelte-i18n'; import { authStore } from '$lib/stores/auth.svelte'; import { favoritesStore } from '$lib/stores/favorites.svelte'; + import { api } from '$lib/api'; interface Location { id: string; @@ -15,16 +16,11 @@ let allLocations = $state([]); let loading = $state(true); - const backendUrl = - typeof window !== 'undefined' - ? (window as any).__PUBLIC_BACKEND_URL__ || 'http://localhost:3025' - : 'http://localhost:3025'; - let favoriteLocations = $derived(allLocations.filter((l) => favoritesStore.isFavorite(l.id))); onMount(async () => { try { - const res = await fetch(`${backendUrl}/locations`); + const res = await fetch(api('/locations')); const data = await res.json(); allLocations = data.locations; } catch (err) { diff --git a/apps/citycorners/apps/web/src/routes/(app)/locations/[id]/+page.svelte b/apps/citycorners/apps/web/src/routes/(app)/locations/[id]/+page.svelte index 93dac4660..7e0c27106 100644 --- a/apps/citycorners/apps/web/src/routes/(app)/locations/[id]/+page.svelte +++ b/apps/citycorners/apps/web/src/routes/(app)/locations/[id]/+page.svelte @@ -5,6 +5,7 @@ import { _ } from 'svelte-i18n'; import { authStore } from '$lib/stores/auth.svelte'; import { favoritesStore } from '$lib/stores/favorites.svelte'; + import { api } from '$lib/api'; interface TimelineEntry { year: string; @@ -27,11 +28,6 @@ let loading = $state(true); let mapContainer: HTMLDivElement; - const backendUrl = - typeof window !== 'undefined' - ? (window as any).__PUBLIC_BACKEND_URL__ || 'http://localhost:3025' - : 'http://localhost:3025'; - const categoryLabels: Record = { sight: 'Sehenswürdigkeit', restaurant: 'Restaurant', @@ -48,7 +44,7 @@ onMount(async () => { try { - const res = await fetch(`${backendUrl}/locations/${$page.params.id}`); + const res = await fetch(api(`/locations/${$page.params.id}`)); const data = await res.json(); location = data.location; } catch (err) { diff --git a/apps/citycorners/apps/web/src/routes/(app)/map/+page.svelte b/apps/citycorners/apps/web/src/routes/(app)/map/+page.svelte index 9d728d99e..c462ccaa4 100644 --- a/apps/citycorners/apps/web/src/routes/(app)/map/+page.svelte +++ b/apps/citycorners/apps/web/src/routes/(app)/map/+page.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import { browser } from '$app/environment'; import { _ } from 'svelte-i18n'; + import { api } from '$lib/api'; interface Location { id: string; @@ -18,11 +19,6 @@ let mapContainer: HTMLDivElement; let map: any = null; - const backendUrl = - typeof window !== 'undefined' - ? (window as any).__PUBLIC_BACKEND_URL__ || 'http://localhost:3025' - : 'http://localhost:3025'; - const categoryColors: Record = { sight: '#2563eb', restaurant: '#dc2626', @@ -40,7 +36,7 @@ onMount(async () => { // Load locations try { - const res = await fetch(`${backendUrl}/locations`); + const res = await fetch(api('/locations')); const data = await res.json(); locations = data.locations; } catch (err) {