Etter Steder er optimalisert

This commit is contained in:
Erol Haagenrud 2026-04-22 06:48:15 +02:00
parent 356a8642b9
commit a0af7a1151
4 changed files with 126 additions and 25 deletions

View file

@ -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

View file

@ -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;

View file

@ -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: () => (
<div className="mt-8 overflow-hidden rounded-[2rem] border border-[#D7DED0] bg-white shadow-sm">
<div className="flex h-[26rem] w-full items-center justify-center bg-[#F3F6EE] text-sm font-bold text-[#617063] sm:h-[34rem]">
Laster kart
</div>
</div>
),
});
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<EnrichedFacility[]>(facilitiesInPlace);
const [filteredFacilities, setFilteredFacilities] = useState<EnrichedFacility[]>(facilities);
useEffect(() => {
setFilteredFacilities(facilitiesInPlace);
}, [facilitiesInPlace]);
setFilteredFacilities(facilities);
}, [facilities]);
const preposition = getPlacePreposition(placeLabel);
const filterHeading = `Filtrer golfbaner ${preposition} ${placeLabel}`;

View file

@ -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
</section>
<PlaceExplorer
facilities={safeData}
facilities={facilitiesInPlace}
placeLabel={place.label}
placeAreaFilter={place.areaFilter}
placeTitle={place.title}