Etter Steder er optimalisert
This commit is contained in:
parent
356a8642b9
commit
a0af7a1151
4 changed files with 126 additions and 25 deletions
107
backend/main.py
107
backend/main.py
|
|
@ -2283,10 +2283,113 @@ async def upsert_facility_rating(request: Request, payload: FacilityRatingUpsert
|
||||||
# --- DATA ENDPOINTS ---
|
# --- DATA ENDPOINTS ---
|
||||||
|
|
||||||
@app.get("/api/facilities")
|
@app.get("/api/facilities")
|
||||||
async def get_facilities():
|
async def get_facilities(summary: bool = False):
|
||||||
"""Henter alle golfanlegg med aggregert banestatus for forsiden."""
|
"""Henter alle golfanlegg med aggregert banestatus for forsiden."""
|
||||||
async with app.state.pool.acquire() as conn:
|
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 f.*, (
|
||||||
SELECT jsonb_agg(cs) FROM (
|
SELECT jsonb_agg(cs) FROM (
|
||||||
SELECT id, name, status FROM courses
|
SELECT id, name, status FROM courses
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export type FacilityRecord = {
|
||||||
id: number;
|
id: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
architect?: string | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
city?: string | null;
|
city?: string | null;
|
||||||
county?: string | null;
|
county?: string | null;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,25 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import dynamic from "next/dynamic";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import FacilitySearch from "@/app/FacilitySearch";
|
import FacilitySearch from "@/app/FacilitySearch";
|
||||||
import PlaceMap from "@/components/PlaceMap";
|
|
||||||
import {
|
import {
|
||||||
type EnrichedFacility,
|
type EnrichedFacility,
|
||||||
type FacilityRecord,
|
|
||||||
enrichFacilities,
|
|
||||||
filterFacilitiesByArea,
|
|
||||||
getPlacePreposition,
|
getPlacePreposition,
|
||||||
} from "@/app/facilityData";
|
} 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 = {
|
type PlaceExplorerProps = {
|
||||||
facilities: FacilityRecord[];
|
facilities: EnrichedFacility[];
|
||||||
placeLabel: string;
|
placeLabel: string;
|
||||||
placeAreaFilter: string;
|
placeAreaFilter: string;
|
||||||
placeTitle: string;
|
placeTitle: string;
|
||||||
|
|
@ -24,15 +31,11 @@ export default function PlaceExplorer({
|
||||||
placeAreaFilter,
|
placeAreaFilter,
|
||||||
placeTitle,
|
placeTitle,
|
||||||
}: PlaceExplorerProps) {
|
}: PlaceExplorerProps) {
|
||||||
const facilitiesInPlace = useMemo(
|
const [filteredFacilities, setFilteredFacilities] = useState<EnrichedFacility[]>(facilities);
|
||||||
() => filterFacilitiesByArea(enrichFacilities(facilities), placeAreaFilter),
|
|
||||||
[facilities, placeAreaFilter]
|
|
||||||
);
|
|
||||||
const [filteredFacilities, setFilteredFacilities] = useState<EnrichedFacility[]>(facilitiesInPlace);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilteredFacilities(facilitiesInPlace);
|
setFilteredFacilities(facilities);
|
||||||
}, [facilitiesInPlace]);
|
}, [facilities]);
|
||||||
|
|
||||||
const preposition = getPlacePreposition(placeLabel);
|
const preposition = getPlacePreposition(placeLabel);
|
||||||
const filterHeading = `Filtrer golfbaner ${preposition} ${placeLabel}`;
|
const filterHeading = `Filtrer golfbaner ${preposition} ${placeLabel}`;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import {
|
||||||
type FacilityRecord,
|
type FacilityRecord,
|
||||||
enrichFacilities,
|
enrichFacilities,
|
||||||
filterFacilitiesByArea,
|
filterFacilitiesByArea,
|
||||||
getAvailablePlaceConfigs,
|
|
||||||
getPlaceConfigFromSlug,
|
getPlaceConfigFromSlug,
|
||||||
getPlacePreposition,
|
getPlacePreposition,
|
||||||
} from "@/app/facilityData";
|
} from "@/app/facilityData";
|
||||||
|
|
@ -23,11 +22,7 @@ import {
|
||||||
} from "@/app/seo";
|
} from "@/app/seo";
|
||||||
|
|
||||||
export const dynamicParams = true;
|
export const dynamicParams = true;
|
||||||
export const dynamic = "force-dynamic";
|
export const revalidate = 3600;
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
|
||||||
return getAvailablePlaceConfigs().map((slug) => ({ slug }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
|
|
@ -63,9 +58,8 @@ export default async function PlacePage({ params }: { params: Promise<{ slug: st
|
||||||
let facilities: FacilityRecord[] = [];
|
let facilities: FacilityRecord[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/facilities`, {
|
const res = await fetch(`${API_URL}/facilities?summary=1`, {
|
||||||
next: { revalidate: 0 },
|
next: { revalidate },
|
||||||
cache: "no-store",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -211,7 +205,7 @@ export default async function PlacePage({ params }: { params: Promise<{ slug: st
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<PlaceExplorer
|
<PlaceExplorer
|
||||||
facilities={safeData}
|
facilities={facilitiesInPlace}
|
||||||
placeLabel={place.label}
|
placeLabel={place.label}
|
||||||
placeAreaFilter={place.areaFilter}
|
placeAreaFilter={place.areaFilter}
|
||||||
placeTitle={place.title}
|
placeTitle={place.title}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue