From a0af7a1151ae94c8200a6158aefaf5e60f881f0f Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Wed, 22 Apr 2026 06:48:15 +0200 Subject: [PATCH] Etter Steder er optimalisert --- backend/main.py | 107 +++++++++++++++++- frontend/src/app/facilityData.ts | 1 + .../src/app/sted/[slug]/PlaceExplorer.tsx | 29 ++--- frontend/src/app/sted/[slug]/page.tsx | 14 +-- 4 files changed, 126 insertions(+), 25 deletions(-) diff --git a/backend/main.py b/backend/main.py index 227f35c..ca84ae9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2283,10 +2283,113 @@ async def upsert_facility_rating(request: Request, payload: FacilityRatingUpsert # --- DATA ENDPOINTS --- @app.get("/api/facilities") -async def get_facilities(): +async def get_facilities(summary: bool = False): """Henter alle golfanlegg med aggregert banestatus for forsiden.""" async with app.state.pool.acquire() as conn: - rows = await conn.fetch(""" + if summary: + rows = await conn.fetch(""" + SELECT + f.id, + f.slug, + f.name, + f.architect, + f.description, + f.city, + f.county, + f.banetype, + f.image_url, + f.phone, + f.website_url, + f.golfbox_booking_url, + f.golfbox_tournament_url, + f.weather_url, + f.lat, + f.lng, + f.golfamore, + f.golfamore_url, + f.nsg_url, + f.greenfee, + f.standard_medlemskap, + f.vtg_pris, + f.vtg_lenke, + f.vtg_beskrivelse, + f.amenities, + f.golfamore_data, + f.nsg_data, + f.vtg_datoer, + f.footnote, + f.footnote_updated_at, + f.status_updated_at, + ( + SELECT jsonb_agg(cs) FROM ( + SELECT id, name, status FROM courses + WHERE facility_id = f.id AND status != 'finnes_ingen_bane_to' + ORDER BY is_main_course DESC, id ASC + ) cs + ) as course_statuses, + ( + SELECT COUNT(*) + FROM holes h + JOIN courses c ON c.id = h.course_id + WHERE c.facility_id = f.id + ) as total_hole_count, + ( + SELECT jsonb_build_object( + '3', COUNT(*) FILTER (WHERE h.par = 3), + '4', COUNT(*) FILTER (WHERE h.par = 4), + '5', COUNT(*) FILTER (WHERE h.par = 5), + '6', COUNT(*) FILTER (WHERE h.par = 6) + ) + FROM holes h + JOIN courses c ON c.id = h.course_id + WHERE c.facility_id = f.id + ) as hole_par_counts, + ( + SELECT MIN((length_value.value)::int) + FROM holes h + JOIN courses c ON c.id = h.course_id + CROSS JOIN LATERAL jsonb_each_text(COALESCE(h.lengths, '{}'::jsonb)) AS length_value(key, value) + WHERE c.facility_id = f.id + AND length_value.key IN ('kortest', 'kort', 'mellomkort', 'mellomlang', 'lang', 'lengst') + AND length_value.value ~ '^[0-9]+$' + AND (length_value.value)::int BETWEEN 30 AND 900 + ) as shortest_hole_meters, + ( + SELECT MAX((length_value.value)::int) + FROM holes h + JOIN courses c ON c.id = h.course_id + CROSS JOIN LATERAL jsonb_each_text(COALESCE(h.lengths, '{}'::jsonb)) AS length_value(key, value) + WHERE c.facility_id = f.id + AND length_value.key IN ('kortest', 'kort', 'mellomkort', 'mellomlang', 'lang', 'lengst') + AND length_value.value ~ '^[0-9]+$' + AND (length_value.value)::int BETWEEN 30 AND 900 + ) as longest_hole_meters, + ( + SELECT jsonb_agg(w_data ORDER BY w_data.day_offset ASC) FROM ( + SELECT + forecast_date, + day_offset, + dry_all_day, + dry_daylight, + precip_mm, + precip_probability_max, + daylight_precip_mm, + daylight_precip_probability_max, + confidence, + source_updated_at, + source_expires_at, + calculated_at + FROM facility_weather_forecast + WHERE facility_id = f.id + ORDER BY day_offset ASC + ) w_data + ) as weather_forecast + FROM facilities f + WHERE COALESCE(f.is_published, TRUE) = TRUE + ORDER BY f.name ASC + """) + else: + rows = await conn.fetch(""" SELECT f.*, ( SELECT jsonb_agg(cs) FROM ( SELECT id, name, status FROM courses diff --git a/frontend/src/app/facilityData.ts b/frontend/src/app/facilityData.ts index 4f87fbb..c7d2294 100755 --- a/frontend/src/app/facilityData.ts +++ b/frontend/src/app/facilityData.ts @@ -9,6 +9,7 @@ export type FacilityRecord = { id: number; slug: string; name: string; + architect?: string | null; description?: string | null; city?: string | null; county?: string | null; diff --git a/frontend/src/app/sted/[slug]/PlaceExplorer.tsx b/frontend/src/app/sted/[slug]/PlaceExplorer.tsx index 114190a..240239a 100644 --- a/frontend/src/app/sted/[slug]/PlaceExplorer.tsx +++ b/frontend/src/app/sted/[slug]/PlaceExplorer.tsx @@ -1,18 +1,25 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import dynamic from "next/dynamic"; +import { useEffect, useState } from "react"; import FacilitySearch from "@/app/FacilitySearch"; -import PlaceMap from "@/components/PlaceMap"; import { type EnrichedFacility, - type FacilityRecord, - enrichFacilities, - filterFacilitiesByArea, getPlacePreposition, } from "@/app/facilityData"; +const PlaceMap = dynamic(() => import("@/components/PlaceMap"), { + loading: () => ( +
+
+ Laster kart… +
+
+ ), +}); + type PlaceExplorerProps = { - facilities: FacilityRecord[]; + facilities: EnrichedFacility[]; placeLabel: string; placeAreaFilter: string; placeTitle: string; @@ -24,15 +31,11 @@ export default function PlaceExplorer({ placeAreaFilter, placeTitle, }: PlaceExplorerProps) { - const facilitiesInPlace = useMemo( - () => filterFacilitiesByArea(enrichFacilities(facilities), placeAreaFilter), - [facilities, placeAreaFilter] - ); - const [filteredFacilities, setFilteredFacilities] = useState(facilitiesInPlace); + const [filteredFacilities, setFilteredFacilities] = useState(facilities); useEffect(() => { - setFilteredFacilities(facilitiesInPlace); - }, [facilitiesInPlace]); + setFilteredFacilities(facilities); + }, [facilities]); const preposition = getPlacePreposition(placeLabel); const filterHeading = `Filtrer golfbaner ${preposition} ${placeLabel}`; diff --git a/frontend/src/app/sted/[slug]/page.tsx b/frontend/src/app/sted/[slug]/page.tsx index 7ab3b1c..7344380 100755 --- a/frontend/src/app/sted/[slug]/page.tsx +++ b/frontend/src/app/sted/[slug]/page.tsx @@ -10,7 +10,6 @@ import { type FacilityRecord, enrichFacilities, filterFacilitiesByArea, - getAvailablePlaceConfigs, getPlaceConfigFromSlug, getPlacePreposition, } from "@/app/facilityData"; @@ -23,11 +22,7 @@ import { } from "@/app/seo"; export const dynamicParams = true; -export const dynamic = "force-dynamic"; - -export async function generateStaticParams() { - return getAvailablePlaceConfigs().map((slug) => ({ slug })); -} +export const revalidate = 3600; export async function generateMetadata({ params, @@ -63,9 +58,8 @@ export default async function PlacePage({ params }: { params: Promise<{ slug: st let facilities: FacilityRecord[] = []; try { - const res = await fetch(`${API_URL}/facilities`, { - next: { revalidate: 0 }, - cache: "no-store", + const res = await fetch(`${API_URL}/facilities?summary=1`, { + next: { revalidate }, }); if (!res.ok) { @@ -211,7 +205,7 @@ export default async function PlacePage({ params }: { params: Promise<{ slug: st