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 f78917c1e..7d093915f 100644 --- a/apps/citycorners/apps/web/src/lib/i18n/locales/de.json +++ b/apps/citycorners/apps/web/src/lib/i18n/locales/de.json @@ -17,7 +17,9 @@ "subtitle": "Sehenswürdigkeiten, Restaurants, Museen und mehr", "all": "Alle", "loading": "Laden...", - "noResults": "Keine Locations gefunden." + "noResults": "Keine Locations gefunden.", + "noResultsCategory": "Keine {category} gefunden.", + "addFirst": "Ersten Ort hinzufügen" }, "categories": { "sight": "Sehenswürdigkeiten", @@ -33,7 +35,12 @@ }, "detail": { "history": "Geschichte", - "openInMaps": "In OpenStreetMap öffnen", + "openInMaps": "OSM", + "showOnMap": "Auf Karte", + "directions": "Route", + "share": "Teilen", + "linkCopied": "Link kopiert!", + "showDetails": "Details", "back": "Zurück zur Übersicht", "notFound": "Location nicht gefunden." }, @@ -47,7 +54,11 @@ }, "map": { "title": "Karte", - "subtitle": "Alle Orte in Konstanz" + "subtitle": "Alle Orte in Konstanz", + "locateMe": "Mein Standort", + "yourLocation": "Du bist hier", + "geolocationNotSupported": "Standortbestimmung wird nicht unterstützt.", + "geolocationError": "Standort konnte nicht ermittelt werden." }, "search": { "placeholder": "Ort suchen...", @@ -101,6 +112,7 @@ "imageUrlPlaceholder": "https://example.com/bild.jpg", "imagePreview": "Bildvorschau", "imageLoadError": "Bild konnte nicht geladen werden.", + "imageRetry": "Erneut versuchen", "geocoding": "Koordinaten werden ermittelt...", "coordinatesFound": "Koordinaten gefunden" }, 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 2ee6fead0..330920ad8 100644 --- a/apps/citycorners/apps/web/src/lib/i18n/locales/en.json +++ b/apps/citycorners/apps/web/src/lib/i18n/locales/en.json @@ -17,7 +17,9 @@ "subtitle": "Sights, restaurants, museums and more", "all": "All", "loading": "Loading...", - "noResults": "No locations found." + "noResults": "No locations found.", + "noResultsCategory": "No {category} found.", + "addFirst": "Add the first place" }, "categories": { "sight": "Sights", @@ -33,7 +35,12 @@ }, "detail": { "history": "History", - "openInMaps": "Open in OpenStreetMap", + "openInMaps": "OSM", + "showOnMap": "On map", + "directions": "Directions", + "share": "Share", + "linkCopied": "Link copied!", + "showDetails": "Details", "back": "Back to overview", "notFound": "Location not found." }, @@ -47,7 +54,11 @@ }, "map": { "title": "Map", - "subtitle": "All places in Konstanz" + "subtitle": "All places in Konstanz", + "locateMe": "My location", + "yourLocation": "You are here", + "geolocationNotSupported": "Geolocation is not supported.", + "geolocationError": "Could not determine location." }, "search": { "placeholder": "Search places...", @@ -101,6 +112,7 @@ "imageUrlPlaceholder": "https://example.com/image.jpg", "imagePreview": "Image preview", "imageLoadError": "Image could not be loaded.", + "imageRetry": "Retry", "geocoding": "Finding coordinates...", "coordinatesFound": "Coordinates found" }, diff --git a/apps/citycorners/apps/web/src/routes/(app)/+page.svelte b/apps/citycorners/apps/web/src/routes/(app)/+page.svelte index 6695b0c71..d1b1f2393 100644 --- a/apps/citycorners/apps/web/src/routes/(app)/+page.svelte +++ b/apps/citycorners/apps/web/src/routes/(app)/+page.svelte @@ -22,6 +22,16 @@ const categoryKeys = ['sight', 'restaurant', 'shop', 'museum']; + let categoryCounts = $derived( + categoryKeys.reduce( + (acc, cat) => { + acc[cat] = locations.filter((l) => l.category === cat).length; + return acc; + }, + {} as Record + ) + ); + let filtered = $derived( selectedCategory ? locations.filter((l) => l.category === selectedCategory) : locations ); @@ -76,7 +86,7 @@ : 'bg-background-card text-foreground-secondary hover:bg-background-card-hover'}" onclick={() => (selectedCategory = null)} > - {$_('home.all')} + {$_('home.all')} ({locations.length}) {#each categoryKeys as cat} {/each} @@ -93,7 +103,31 @@ {#if loading}

{$_('home.loading')}

{:else if filtered.length === 0} -

{$_('home.noResults')}

+
+ {selectedCategory === 'restaurant' + ? '🍽️' + : selectedCategory === 'museum' + ? '🏛️' + : selectedCategory === 'shop' + ? '🛍️' + : selectedCategory === 'sight' + ? '🏰' + : '📍'} +

+ {#if selectedCategory} + {$_('home.noResultsCategory', { + values: { category: $_(`categories.${selectedCategory}`) }, + })} + {:else} + {$_('home.noResults')} + {/if} +

+ + {$_('home.addFirst')} + +
{:else}
{#each filtered as location} @@ -102,7 +136,12 @@ class="group relative overflow-hidden rounded-xl border border-border bg-background-card transition-shadow hover:shadow-lg" > {#if location.imageUrl} - {location.name} + {location.name} {:else}
📍 diff --git a/apps/citycorners/apps/web/src/routes/(app)/add/+page.svelte b/apps/citycorners/apps/web/src/routes/(app)/add/+page.svelte index feb9d811f..5d3235ac0 100644 --- a/apps/citycorners/apps/web/src/routes/(app)/add/+page.svelte +++ b/apps/citycorners/apps/web/src/routes/(app)/add/+page.svelte @@ -356,7 +356,18 @@ />
{:else if imageError} -

{$_('add.imageLoadError')}

+
+

{$_('add.imageLoadError')}

+ +
{/if}
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 7e0c27106..c26a30240 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 @@ -27,13 +27,7 @@ let location = $state(null); let loading = $state(true); let mapContainer: HTMLDivElement; - - const categoryLabels: Record = { - sight: 'Sehenswürdigkeit', - restaurant: 'Restaurant', - shop: 'Laden', - museum: 'Museum', - }; + let shareSuccess = $state(false); const categoryColors: Record = { sight: '#2563eb', @@ -87,6 +81,23 @@ initMap(); }); + + async function handleShare() { + const url = window.location.href; + const title = location?.name || 'CityCorners'; + + if (navigator.share) { + try { + await navigator.share({ title, url }); + } catch { + // User cancelled share + } + } else { + await navigator.clipboard.writeText(url); + shareSuccess = true; + setTimeout(() => (shareSuccess = false), 2000); + } + } @@ -121,7 +132,7 @@ {/if} - + - {#if authStore.isAuthenticated} -
+ +
+ + + {#if authStore.isAuthenticated} -
- {/if} + {/if} +
@@ -173,7 +217,7 @@ class="rounded-full px-3 py-1 text-sm font-medium text-white backdrop-blur-sm" style="background: {categoryColors[location.category] || '#6b7280'}cc" > - {categoryLabels[location.category] || location.category} + {$_(`category.${location.category}`)}
@@ -209,31 +253,73 @@

{location.description}

- + {#if location.latitude && location.longitude}
{/if} @@ -243,7 +329,7 @@

{$_('detail.history')}

{#each location.timeline as entry, i} -
+
{#if i < location.timeline!.length - 1}
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 c462ccaa4..9326afdfc 100644 --- a/apps/citycorners/apps/web/src/routes/(app)/map/+page.svelte +++ b/apps/citycorners/apps/web/src/routes/(app)/map/+page.svelte @@ -18,6 +18,8 @@ let locations = $state([]); let mapContainer: HTMLDivElement; let map: any = null; + let locating = $state(false); + let locationError = $state(''); const categoryColors: Record = { sight: '#2563eb', @@ -26,15 +28,7 @@ museum: '#9333ea', }; - const categoryLabels: Record = { - sight: 'Sehenswürdigkeit', - restaurant: 'Restaurant', - shop: 'Laden', - museum: 'Museum', - }; - onMount(async () => { - // Load locations try { const res = await fetch(api('/locations')); const data = await res.json(); @@ -45,11 +39,9 @@ if (!browser) return; - // Dynamically import Leaflet (client-side only) const L = await import('leaflet'); await import('leaflet/dist/leaflet.css'); - // Center on Konstanz map = L.map(mapContainer).setView([47.6603, 9.1757], 14); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { @@ -57,7 +49,6 @@ maxZoom: 19, }).addTo(map); - // Add markers for locations with coordinates for (const loc of locations) { if (loc.latitude && loc.longitude) { const color = categoryColors[loc.category] || '#6b7280'; @@ -74,14 +65,53 @@ marker.bindPopup(`
${loc.name} -
${categoryLabels[loc.category] || loc.category}
+
${$_(`category.${loc.category}`)}

${loc.description.substring(0, 100)}...

- Details → + ${$_('detail.showDetails')} →
`); } } }); + + function handleLocateMe() { + if (!browser || !map || !navigator.geolocation) { + locationError = $_('map.geolocationNotSupported'); + return; + } + + locating = true; + locationError = ''; + + navigator.geolocation.getCurrentPosition( + async (pos) => { + const { latitude, longitude } = pos.coords; + const L = await import('leaflet'); + + map.setView([latitude, longitude], 16); + + // Add user marker + const userIcon = L.divIcon({ + className: 'custom-marker', + html: `
`, + iconSize: [16, 16], + iconAnchor: [8, 8], + }); + + L.marker([latitude, longitude], { icon: userIcon }) + .addTo(map) + .bindPopup($_('map.yourLocation')) + .openPopup(); + + locating = false; + }, + () => { + locationError = $_('map.geolocationError'); + locating = false; + }, + { enableHighAccuracy: true, timeout: 10000 } + ); + } @@ -90,16 +120,42 @@
-
-

{$_('map.title')}

-

{$_('map.subtitle')}

+
+
+

{$_('map.title')}

+

{$_('map.subtitle')}

+
+
+ {#if locationError} +
{locationError}
+ {/if} +
{#each Object.entries(categoryColors) as [key, color]}
- {categoryLabels[key]} + {$_(`category.${key}`)}
{/each}