diff --git a/apps/citycorners/apps/web/src/lib/data/queries.ts b/apps/citycorners/apps/web/src/lib/data/queries.ts index 9d37d31e4..2ebe1637d 100644 --- a/apps/citycorners/apps/web/src/lib/data/queries.ts +++ b/apps/citycorners/apps/web/src/lib/data/queries.ts @@ -109,3 +109,71 @@ export function getLocationCountByCity(locations: LocalLocation[]): Map; + topCategories: { category: string; count: number }[]; + contributorCount: number; + hasCoordinates: number; + recentLocations: LocalLocation[]; +} + +/** Compute stats for a city's locations. */ +export function getCityStats(locations: LocalLocation[]): CityStats { + const categoryCounts: Record = {}; + const contributors = new Set(); + let hasCoordinates = 0; + + for (const loc of locations) { + categoryCounts[loc.category] = (categoryCounts[loc.category] || 0) + 1; + if ((loc as any).createdBy) contributors.add((loc as any).createdBy); + if (loc.latitude && loc.longitude) hasCoordinates++; + } + + const topCategories = Object.entries(categoryCounts) + .map(([category, count]) => ({ category, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 5); + + const recentLocations = [...locations] + .sort((a, b) => { + const aTime = (a as any).createdAt ? new Date((a as any).createdAt).getTime() : 0; + const bTime = (b as any).createdAt ? new Date((b as any).createdAt).getTime() : 0; + return bTime - aTime; + }) + .slice(0, 3); + + return { + locationCount: locations.length, + categoryCounts, + topCategories, + contributorCount: contributors.size, + hasCoordinates, + recentLocations, + }; +} + +/** Stats summary for the city discovery page. */ +export interface PlatformStats { + totalCities: number; + totalLocations: number; + totalContributors: number; +} + +/** Compute platform-wide stats. */ +export function getPlatformStats(cities: LocalCity[], locations: LocalLocation[]): PlatformStats { + const contributors = new Set(); + for (const loc of locations) { + if ((loc as any).createdBy) contributors.add((loc as any).createdBy); + } + for (const city of cities) { + if (city.createdBy) contributors.add(city.createdBy); + } + return { + totalCities: cities.length, + totalLocations: locations.length, + totalContributors: contributors.size, + }; +} 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 34e4a81df..68d1db538 100644 --- a/apps/citycorners/apps/web/src/lib/i18n/locales/de.json +++ b/apps/citycorners/apps/web/src/lib/i18n/locales/de.json @@ -20,7 +20,16 @@ "add": "Stadt hinzufügen", "empty": "Noch keine Städte. Sei der Erste!", "locationsCount": "{count} Orte", - "noLocationsYet": "Noch keine Orte" + "noLocationsYet": "Noch keine Orte", + "contributors": "{count} Beitragende", + "contributorsOne": "1 Beitragender", + "onMap": "{count} auf der Karte", + "recentlyAdded": "Zuletzt hinzugefügt", + "stats": "Statistiken", + "totalCities": "{count} Städte", + "totalLocations": "{count} Orte", + "totalContributors": "{count} Beitragende", + "topCategories": "Top-Kategorien" }, "cityAdd": { "title": "Neue Stadt anlegen", 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 3f64c9046..75b2c0686 100644 --- a/apps/citycorners/apps/web/src/lib/i18n/locales/en.json +++ b/apps/citycorners/apps/web/src/lib/i18n/locales/en.json @@ -20,7 +20,16 @@ "add": "Add a city", "empty": "No cities yet. Be the first!", "locationsCount": "{count} places", - "noLocationsYet": "No places yet" + "noLocationsYet": "No places yet", + "contributors": "{count} contributors", + "contributorsOne": "1 contributor", + "onMap": "{count} on the map", + "recentlyAdded": "Recently added", + "stats": "Statistics", + "totalCities": "{count} cities", + "totalLocations": "{count} places", + "totalContributors": "{count} contributors", + "topCategories": "Top categories" }, "cityAdd": { "title": "Add a new city", diff --git a/apps/citycorners/apps/web/src/routes/(app)/+page.svelte b/apps/citycorners/apps/web/src/routes/(app)/+page.svelte index 7ecd355b3..6fcad4843 100644 --- a/apps/citycorners/apps/web/src/routes/(app)/+page.svelte +++ b/apps/citycorners/apps/web/src/routes/(app)/+page.svelte @@ -7,6 +7,9 @@ useAllLocations, searchCities, getLocationCountByCity, + getPlatformStats, + filterByCity, + getCityStats, } from '$lib/data/queries'; const allCities = useAllCities(); @@ -15,6 +18,7 @@ let searchQuery = $state(''); let locationCounts = $derived(getLocationCountByCity(allLocations.value)); + let platformStats = $derived(getPlatformStats(allCities.value, allLocations.value)); let filtered = $derived(searchCities(allCities.value, searchQuery)); @@ -41,6 +45,39 @@ {/if} + +{#if platformStats.totalCities > 0} +
+
+ 🏙️ +
+

{platformStats.totalCities}

+

{$_('nav.cities')}

+
+
+
+ 📍 +
+

{platformStats.totalLocations}

+

{$_('home.title')}

+
+
+ {#if platformStats.totalContributors > 0} +
+ 👥 +
+

{platformStats.totalContributors}

+

+ {$_('cities.totalContributors', { + values: { count: platformStats.totalContributors }, + })} +

+
+
+ {/if} +
+{/if} +
{/if} -
- {@const count = locationCounts.get(city.id) || 0} - {#if count > 0} - {$_('cities.locationsCount', { values: { count } })} - {:else} - {$_('cities.noLocationsYet')} + {@const count = locationCounts.get(city.id) || 0} + {@const cityStats = getCityStats(filterByCity(allLocations.value, city.id))} +
+ + + + + {count} + + {#if cityStats.contributorCount > 0} + + + + + {cityStats.contributorCount} + {/if} + {#each cityStats.topCategories.slice(0, 2) as { category }} + + {$_(`categories.${category}`)} + + {/each}
diff --git a/apps/citycorners/apps/web/src/routes/(app)/cities/[slug]/+page.svelte b/apps/citycorners/apps/web/src/routes/(app)/cities/[slug]/+page.svelte index 90cb95999..6a739ee49 100644 --- a/apps/citycorners/apps/web/src/routes/(app)/cities/[slug]/+page.svelte +++ b/apps/citycorners/apps/web/src/routes/(app)/cities/[slug]/+page.svelte @@ -10,6 +10,7 @@ getFavoriteIds, filterByCity, filterByCategory, + getCityStats, } from '$lib/data/queries'; import { isOpenNow } from '$lib/opening-hours'; import type { LocalCity } from '$lib/data/local-store'; @@ -51,6 +52,7 @@ ); let filtered = $derived(filterByCategory(cityLocations, selectedCategory)); + let stats = $derived(getCityStats(cityLocations)); let citySlug = $derived($page.params.slug); @@ -97,6 +99,115 @@ + +{#if stats.locationCount > 0} +
+
+ +
+
+ + + + +
+
+

{stats.locationCount}

+

+ {$_('cities.locationsCount', { values: { count: stats.locationCount } })} +

+
+
+ + + {#if stats.hasCoordinates > 0} +
+
+ + + +
+
+

{stats.hasCoordinates}

+

+ {$_('cities.onMap', { values: { count: stats.hasCoordinates } })} +

+
+
+ {/if} + + + {#if stats.contributorCount > 0} +
+
+ + + +
+
+

{stats.contributorCount}

+

+ {$_('cities.contributors', { values: { count: stats.contributorCount } })} +

+
+
+ {/if} +
+ + + {#if stats.topCategories.length > 1} +
+ {#each stats.topCategories as { category, count }} + + {$_(`categories.${category}`)} + {count} + + {/each} +
+ {/if} +
+{/if} +