diff --git a/backend/main.py b/backend/main.py index 5ebc5df..673b47a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1823,6 +1823,14 @@ async def update_scrape_settings(facility_id: int, settings: ScrapeSettingsUpdat async def update_facility_full(facility_id: int, request: Request): """Dynamisk endpoint som oppdaterer anlegg, baner og hull (den fulle editoren).""" data = await request.json() + + legacy_field_aliases = { + 'vtg_presentasjon': 'vtg_beskrivelse', + 'vtg_kursdatoer': 'vtg_datoer', + } + for legacy_field, canonical_field in legacy_field_aliases.items(): + if legacy_field in data and canonical_field not in data: + data[canonical_field] = data[legacy_field] # Felter som er trygge å oppdatere manuelt på anlegget allowed_fields = [ @@ -1834,7 +1842,7 @@ async def update_facility_full(facility_id: int, request: Request): 'nsg_url', 'nsg_data', 'golfamore', 'golfamore_data', 'navn_standard_medlemskap', 'standard_medlemskap', 'standard_medlemskap_kommentarer', 'navn_rimeligste_alternativ', 'rimeligste_alternativ', 'medlemskap_url', - 'vtg_presentasjon', 'vtg_lenke', 'vtg_pris', 'vtg_kursdatoer', + 'vtg_beskrivelse', 'vtg_lenke', 'vtg_pris', 'vtg_datoer', 'guest_requirements', 'scrape_method', 'scrape_status_url', 'social_links', 'footnote', 'cooperating_clubs', 'membership_draft', 'membership_updated_at', 'greenfee_url', 'greenfee_draft', 'greenfee_updated_at', 'scrape_status_selector', 'vtg_lenke', @@ -1898,8 +1906,10 @@ async def update_facility_full(facility_id: int, request: Request): await conn.execute(query, *values) # 2. OPPDATER BANER (COURSES) OG HULL (HOLES) - courses = data.get('courses', []) + courses = data.get('courses') or [] for course in courses: + if not course: + continue course_id = course.get('id') if course_id: # Rens datoformat for PostgreSQL (håndterer Next.js date input) @@ -1923,11 +1933,13 @@ async def update_facility_full(facility_id: int, request: Request): """, course.get('name'), course.get('par'), course.get('length_meters'), course.get('architect'), course.get('status'), course.get('is_main_course'), - json.dumps(course.get('tee_boxes', {})), valid_until, course_id, facility_id) + json.dumps(course.get('tee_boxes') or {}), valid_until, course_id, facility_id) # 3. OPPDATER HULL PÅ BANEN (HOLES) - holes = course.get('holes', []) + holes = course.get('holes') or [] for hole in holes: + if not hole: + continue hole_id = hole.get('id') if hole_id: await conn.execute(""" @@ -1936,7 +1948,7 @@ async def update_facility_full(facility_id: int, request: Request): WHERE id=$4 AND course_id=$5 """, hole.get('par'), hole.get('hcp_index'), - json.dumps(hole.get('lengths', {})), hole_id, course_id) + json.dumps(hole.get('lengths') or {}), hole_id, course_id) return {"status": "success", "message": "Anlegg, baner og scorekort ble oppdatert."} diff --git a/docs/design-system.md b/docs/design-system.md new file mode 100644 index 0000000..097c733 --- /dev/null +++ b/docs/design-system.md @@ -0,0 +1,173 @@ +# TeeOff designsystem + +Dette dokumentet beskriver den visuelle profilen slik løsningen faktisk er bygget nå, og setter en tydelig standard for videre arbeid. Utgangspunktet er logoen og de etablerte fargetonene `#8BC24A` og `#FF5722`. + +## 1. Designprofil + +TeeOff skal oppleves som golfnært, redaksjonelt og praktisk samtidig. Profilen er ikke luksus-klubb eller generisk SaaS; den er bygget rundt natur, banestatus, kart, redaksjonelle historier og raske verktøy. + +Kjennetegn: + +- Jordnær og nordisk, ikke steril. +- Tydelig redaksjonell typografi med kraftige overskrifter. +- Klare signalfarger for status, handlinger og fremdrift. +- Store flater, avrundede kort og tydelig lagdeling. +- Bilder skal gi stemning, mens data og filtre skal gi kontroll. + +## 2. Merkevaregrunnlag + +Primære merkevarefarger: + +- Brand green: `#8BC24A` +- Brand orange: `#FF5722` + +Støttefarger i dagens kodebase: + +- Pine 900: `#25312A` +- Pine/ink: `#112015` +- Pine 700: `#39443B` +- Mist 50: `#F3F6EE` +- Surface: `#FFFFFF` +- Ink muted: `#617063` +- Orange dark hover: `#C94F2D` +- Status closed: `#B6473D` +- Status winter: `#D2A63A` + +Bruk: + +- `#8BC24A` brukes til bekreftelse, aksenter, aktive valg og små signalflater. +- `#FF5722` brukes til primære CTA-er, lenker, høy oppmerksomhet og varme kontraster. +- `#25312A` og `#112015` er base for header, mørke knapper, overlay og typografisk tyngde. +- `#F3F6EE` og hvitt holder store flater lette og gir plass til bilder og data. + +## 3. Typografi + +Etablert fontbruk i appen: + +- Display: `Oswald` +- UI/brødtekst: `Mulish` + +Regler: + +- Overskrifter skal bruke display-font og tåle høy vekt, store størrelser og tett linjehøyde. +- Brødtekst, skjemaer, kortmetadata og hjelpetekst skal bruke UI-font. +- Eyebrows, små etiketter og filterlabels skal ofte være uppercase med tydelig tracking. +- Lange forklaringer skal være lettleste, ikke komprimerte. + +Tone i typografi: + +- Overskrifter skal være tydelige og selvsikre. +- Brødtekst skal være enkel, presis og nytteorientert. +- UI-tekst skal prioritere klarhet foran kreativ formulering. + +## 4. Layout og form + +Gjeldende formspråk: + +- Store avrundinger: typisk `1rem` til `2rem` +- Myke kort med tynn ramme og lett skygge +- Bred desktop-ramme: `max-w-[1400px]` +- Luftig spacing med tydelig seksjonsdeling + +Regler: + +- Kort, filtre og paneler skal føles som fysiske flater på toppen av en myk bakgrunn. +- Unngå harde svarte skiller og kompakte admin-tabeller i offentlig UI. +- Bruk få, tydelige lag i stedet for mange små bokser inni hverandre. +- Mobil skal prioriteres med vertikal rytme og tydelige trykkflater. + +## 5. Komponentprinsipper + +### Header og navigasjon + +- Header skal ligge mørkt og stabilt over innholdet. +- Desktop-nav kan være kompakt og editorial. +- Mobilmeny skal være enkel å skanne, med viktigste innhold først og områdenavigasjon som egen blokk. + +### Knapper og lenker + +- Primær handling: orange fyll. +- Sekundær handling: mørk fyll eller lys outline, avhengig av bakgrunn. +- Vage CTA-er som `Se anlegg` bør unngås. Bruk heller presise handlinger som `Baneprofil`, `Les artikkelen` eller `Innmelding`. +- I frontend skal dette som utgangspunkt mappes til delte varianter i `globals.css`: +- `btn-primary` for viktigste handling i en seksjon. +- `btn-secondary` for sekundære handlinger på lyse flater. +- `btn-secondary-dark` for sekundære handlinger oppå bilder og mørke flater. +- `btn-ink` kun når en mørk handling bevisst skal være underordnet en orange primærknapp. + +### Kort + +- Kort skal bære både bilde, status og nytteinformasjon. +- Statuser skal vises som badges, ikke gjemmes i brødtekst. +- Kort skal ha tydelig hover-respons på desktop, men uten overdreven animasjon. + +### Filtre + +- Filtre er en kjernefunksjon og skal føles som arbeidsverktøy, ikke pynt. +- Felter skal være store nok til fingerbruk på mobil. +- Standardvalg må være tydelige og beskrive resultatet presist. + +## 6. Bilder og visuell stemning + +- Store banebilder er en sentral del av identiteten. +- Bilder skal fremheve landskap, hull, vær og opplevelse. +- Overlays skal mørkne nok til at tekst alltid er lesbar. +- Unngå generiske stock-bilder og sterile dekorflater. + +## 7. Bevegelse + +- Motion skal være diskret og funksjonell. +- Tillatte virkemidler: +- fade mellom hero-bilder +- lett hover-løft på kort +- tydelig overgang på knapper og lenker + +Unngå: + +- tung parallax +- unødvendige mikroanimasjoner på alt +- dekorativ motion som konkurrerer med innholdet + +## 8. Språk og begreper + +Anbefalt standard: + +- Bruk `Golfbaner` i navigasjon, SEO, lister, overskrifter og publikumsvendt oppdagelsesinnhold. +- Bruk `Baneprofil` som CTA til detaljsiden. +- Bruk `golfanlegg` bare når man faktisk mener hele anlegget som fysisk eller organisatorisk enhet. + +Begrunnelse: + +- `Golfbaner` matcher brukerintensjon bedre i søk og i vanlig språkbruk. +- `golfanlegg` er mer presist, men mer teknisk og mindre naturlig i navigasjon og markedsføring. + +## 9. Designregler for nye sider + +- Start med én tydelig hovedhandling per seksjon. +- Bruk brand green til status og systembekreftelse, ikke som eneste CTA-farge. +- Bruk brand orange når noe skal klikkes, åpnes eller prioriteres. +- Hold bakgrunnene lyse, men aldri flate og døde hvis siden trenger karakter. +- Sørg for at kort, badges og filtre bruker samme radius- og skyggefamilie som resten av siden. +- Hvis en side føles som et adminverktøy, må den enten flyttes til admin eller gis en tydeligere offentlig redaksjonell behandling. + +## 10. Ikke gjør dette + +- Ikke innfør nye tilfeldige grønnfarger ved siden av merkevaregrønn. +- Ikke bruk lilla, blågrå SaaS-paletter eller standard dark mode som bryter med merkevaren. +- Ikke bruk generiske CTA-tekster når mer presis tekst er mulig. +- Ikke bygg nye offentlige sider med tett tabellfølelse når kort eller seksjoner fungerer bedre. +- Ikke bytt fontfamilier uten en bevisst oppdatering av hele profilen. + +## 11. Kilde i kode + +Dette dokumentet er forankret i nåværende implementasjon, spesielt: + +- `frontend/src/app/globals.css` +- `frontend/src/app/layout.tsx` +- `frontend/src/components/Header.tsx` +- `frontend/src/app/HeroSlider.tsx` +- `frontend/src/app/FacilitySearch.tsx` +- `frontend/src/app/banebesok/page.tsx` +- `frontend/src/app/meninger/page.tsx` + +Ved senere redesign skal dette dokumentet oppdateres samtidig som design-tokenene eller hovedmønstrene endres. diff --git a/frontend/src/app/FacilitySearch.tsx b/frontend/src/app/FacilitySearch.tsx index dbb2247..a0c501f 100755 --- a/frontend/src/app/FacilitySearch.tsx +++ b/frontend/src/app/FacilitySearch.tsx @@ -572,7 +572,7 @@ export default function FacilitySearch({ - + @@ -620,8 +620,8 @@ export default function FacilitySearch({ setSpecialFilter(""); setSortMethod(userLocation ? "dist" : "updated"); }} - className={`mt-[1.72rem] h-[52px] rounded-2xl px-5 text-[11px] font-extrabold uppercase tracking-[0.2em] transition ${ - variant === "home" ? "bg-[#FF5722] text-white hover:bg-[#C94F2D]" : "bg-[#25312A] text-white hover:bg-[#39443B]" + className={`btn btn-md mt-[1.72rem] h-[52px] ${ + variant === "home" ? "btn-primary" : "btn-secondary" }`} > Nullstill @@ -774,7 +774,7 @@ export default function FacilitySearch({ )} - Se anlegg + Baneprofil diff --git a/frontend/src/app/HeroSlider.tsx b/frontend/src/app/HeroSlider.tsx index 3bca153..eb3f125 100755 --- a/frontend/src/app/HeroSlider.tsx +++ b/frontend/src/app/HeroSlider.tsx @@ -139,7 +139,7 @@ export default function HeroSlider({ facilities }: { facilities: Facility[] }) {
{sliderItems[currentIndex].name} diff --git a/frontend/src/app/admin/artikler/page.tsx b/frontend/src/app/admin/artikler/page.tsx index 662059c..969113a 100644 --- a/frontend/src/app/admin/artikler/page.tsx +++ b/frontend/src/app/admin/artikler/page.tsx @@ -395,14 +395,14 @@ export default function AdminArticlesPage() { type="button" onClick={handleSeedImported} disabled={isSeeding} - className="rounded-full border border-[#11280f]/10 bg-white px-5 py-3 text-[11px] font-black uppercase tracking-[0.16em] text-[#11280f] transition hover:border-[#8BC34A] disabled:opacity-50" + className="btn btn-md btn-secondary disabled:opacity-50" > {isSeeding ? "Importerer..." : "Seed fra import"} @@ -617,7 +617,7 @@ export default function AdminArticlesPage() { type="button" onClick={() => heroImageInputRef.current?.click()} disabled={isUploadingHeroImages} - className="rounded-full border border-[#112015]/10 bg-white px-4 py-2 text-[10px] font-black uppercase tracking-[0.16em] text-[#112015] transition hover:border-[#8BC34A] disabled:cursor-not-allowed disabled:opacity-50" + className="btn btn-sm btn-secondary disabled:cursor-not-allowed disabled:opacity-50" > {isUploadingHeroImages ? "Laster opp..." : "Last opp bilder"} @@ -687,7 +687,7 @@ export default function AdminArticlesPage() { type="button" onClick={handleSave} disabled={isSaving} - className="rounded-full bg-[#112015] px-5 py-3 text-[11px] font-black uppercase tracking-[0.16em] text-white transition hover:bg-[#25312A] disabled:opacity-50" + className="btn btn-md btn-primary disabled:opacity-50" > {isSaving ? "Lagrer..." : "Lagre artikkel"} @@ -696,7 +696,7 @@ export default function AdminArticlesPage() { type="button" onClick={handleDelete} disabled={isDeleting} - className="rounded-full border border-red-200 bg-red-50 px-5 py-3 text-[11px] font-black uppercase tracking-[0.16em] text-red-700 transition hover:bg-red-100 disabled:opacity-50" + className="btn btn-md btn-danger disabled:opacity-50" > {isDeleting ? "Sletter..." : "Slett artikkel"} @@ -705,7 +705,7 @@ export default function AdminArticlesPage() { Åpne offentlig side @@ -714,7 +714,7 @@ export default function AdminArticlesPage() { Åpne baneprofil diff --git a/frontend/src/app/admin/greenfee/page.tsx b/frontend/src/app/admin/greenfee/page.tsx index 82fb4cc..c28cd7e 100644 --- a/frontend/src/app/admin/greenfee/page.tsx +++ b/frontend/src/app/admin/greenfee/page.tsx @@ -119,7 +119,7 @@ export default function GreenfeeWasher() {

Greenfee-Vaskeriet

Sjekk at prisene gir mening før publisering.

- @@ -143,7 +143,7 @@ export default function GreenfeeWasher() {

{draft.name} ID: {draft.id}

- Sjekk Nettside ↗ + Sjekk Nettside ↗
{draft.greenfee_draft?.ai_begrunnelse && ( @@ -180,7 +180,7 @@ export default function GreenfeeWasher() { updateField(draft.id, idx, 'priskategori', e.target.value)} placeholder="Kategori" /> updateField(draft.id, idx, 'pris_voksne', e.target.value)} placeholder="Voksen" /> updateField(draft.id, idx, 'pris_junior', e.target.value)} placeholder="Junior" /> - +
))} diff --git a/frontend/src/app/admin/medlemskap/page.tsx b/frontend/src/app/admin/medlemskap/page.tsx index f413caa..cd66eea 100644 --- a/frontend/src/app/admin/medlemskap/page.tsx +++ b/frontend/src/app/admin/medlemskap/page.tsx @@ -96,7 +96,7 @@ export default function MembershipWasher() { diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index bb02204..2c9648f 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -113,8 +113,8 @@ const InlineEdit = ({ facilityId, field, initialValue, onSave }: { facilityId: n