From c0dee2f6622806e85f226f420fabab8677e2eec9 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 16 Apr 2026 12:45:34 +0200 Subject: [PATCH] Remove export and dump artifacts from repository --- .gitignore | 2 + fil-tre-3.txt | 796 ------------------ fil-tre.txt | 735 ---------------- frontend/src/struktur_dump.txt | 0 kode_eksport_1/backend_create_admin_py.txt | 64 -- kode_eksport_1/backend_import_gallery_py.txt | 111 --- .../backend_import_nye_felter_py.txt | 150 ---- kode_eksport_1/backend_import_urls_py.txt | 101 --- kode_eksport_1/backend_import_wp_py.txt | 157 ---- kode_eksport_1/backend_main_py.txt | 638 -------------- .../backend_scrape_golfamore1_3_py.txt | 124 --- kode_eksport_1/backend_scrape_greenfee_py.txt | 173 ---- .../backend_scrape_membership_py.txt | 168 ---- kode_eksport_1/backend_scrape_nsg_3_py.txt | 96 --- kode_eksport_1/backend_scrape_status_py.txt | 332 -------- kode_eksport_1/backend_scrape_vtg_py.txt | 161 ---- kode_eksport_1/backend_sync_greenfee_py.txt | 79 -- kode_eksport_1/backend_test_gemini_py.txt | 116 --- kode_eksport_1/backend_test_login_py.txt | 47 -- kode_eksport_1/backend_update_admin_py.txt | 85 -- kode_eksport_1/eksport_script_py.txt | 72 -- kode_eksport_1/frontend_next-env_d_ts.txt | 6 - kode_eksport_1/frontend_next_config_ts.txt | 7 - .../frontend_src_app_FacilitySearch_tsx.txt | 217 ----- .../frontend_src_app_HeroSlider_tsx.txt | 130 --- ...ontend_src_app_admin_greenfee_page_tsx.txt | 203 ----- .../frontend_src_app_admin_login_page_tsx.txt | 103 --- ...tend_src_app_admin_medlemskap_page_tsx.txt | 179 ---- .../frontend_src_app_admin_page_tsx.txt | 468 ---------- ..._rediger_[slug]_EditFacilityClient_tsx.txt | 637 -------------- ..._src_app_admin_rediger_[slug]_page_tsx.txt | 20 - .../frontend_src_app_admin_vtg_page_tsx.txt | 208 ----- ...app_golfbaner_[slug]_CourseDisplay_tsx.txt | 206 ----- ...olfbaner_[slug]_FacilityDetailView_tsx.txt | 543 ------------ ...tend_src_app_golfbaner_[slug]_page_tsx.txt | 17 - .../frontend_src_app_layout_tsx.txt | 19 - kode_eksport_1/frontend_src_app_page_tsx.txt | 40 - .../frontend_src_components_Header_tsx.txt | 45 - ..._src_components_ScrapeMethodSelect_tsx.txt | 71 -- .../frontend_src_config_constants_ts.txt | 42 - kode_eksport_1/frontend_src_middleware_ts.txt | 36 - kode_eksport_3/backend_create_admin_py.txt | 64 -- kode_eksport_3/backend_import_gallery_py.txt | 111 --- .../backend_import_nye_felter_py.txt | 150 ---- kode_eksport_3/backend_import_urls_py.txt | 103 --- kode_eksport_3/backend_import_wp_py.txt | 157 ---- kode_eksport_3/backend_main_py.txt | 663 --------------- .../backend_scrape_golfamore1_3_py.txt | 124 --- kode_eksport_3/backend_scrape_greenfee_py.txt | 173 ---- .../backend_scrape_membership_py.txt | 168 ---- kode_eksport_3/backend_scrape_nsg_3_py.txt | 96 --- kode_eksport_3/backend_scrape_status_py.txt | 332 -------- kode_eksport_3/backend_scrape_vtg_py.txt | 161 ---- kode_eksport_3/backend_sync_greenfee_py.txt | 79 -- kode_eksport_3/backend_test_gemini_py.txt | 116 --- kode_eksport_3/backend_test_login_py.txt | 47 -- kode_eksport_3/backend_update_admin_py.txt | 85 -- kode_eksport_3/eksport_script_py.txt | 72 -- kode_eksport_3/frontend_next-env_d_ts.txt | 6 - kode_eksport_3/frontend_next_config_ts.txt | 7 - .../frontend_src_app_FacilitySearch_tsx.txt | 217 ----- .../frontend_src_app_HeroSlider_tsx.txt | 130 --- ...ontend_src_app_admin_greenfee_page_tsx.txt | 203 ----- .../frontend_src_app_admin_login_page_tsx.txt | 103 --- ...tend_src_app_admin_medlemskap_page_tsx.txt | 179 ---- .../frontend_src_app_admin_page_tsx.txt | 468 ---------- ..._rediger_[slug]_EditFacilityClient_tsx.txt | 637 -------------- ..._src_app_admin_rediger_[slug]_page_tsx.txt | 20 - .../frontend_src_app_admin_vtg_page_tsx.txt | 208 ----- ...app_golfbaner_[slug]_CourseDisplay_tsx.txt | 206 ----- ...olfbaner_[slug]_FacilityDetailView_tsx.txt | 543 ------------ ...tend_src_app_golfbaner_[slug]_page_tsx.txt | 17 - .../frontend_src_app_layout_tsx.txt | 19 - kode_eksport_3/frontend_src_app_page_tsx.txt | 40 - .../frontend_src_components_Header_tsx.txt | 45 - ..._src_components_ScrapeMethodSelect_tsx.txt | 71 -- .../frontend_src_config_constants_ts.txt | 42 - kode_eksport_3/frontend_src_middleware_ts.txt | 36 - struktur2_dump.txt | 639 -------------- struktur3_dump.txt | 639 -------------- 80 files changed, 2 insertions(+), 14578 deletions(-) delete mode 100644 fil-tre-3.txt delete mode 100644 fil-tre.txt delete mode 100644 frontend/src/struktur_dump.txt delete mode 100644 kode_eksport_1/backend_create_admin_py.txt delete mode 100644 kode_eksport_1/backend_import_gallery_py.txt delete mode 100644 kode_eksport_1/backend_import_nye_felter_py.txt delete mode 100644 kode_eksport_1/backend_import_urls_py.txt delete mode 100644 kode_eksport_1/backend_import_wp_py.txt delete mode 100644 kode_eksport_1/backend_main_py.txt delete mode 100644 kode_eksport_1/backend_scrape_golfamore1_3_py.txt delete mode 100644 kode_eksport_1/backend_scrape_greenfee_py.txt delete mode 100644 kode_eksport_1/backend_scrape_membership_py.txt delete mode 100644 kode_eksport_1/backend_scrape_nsg_3_py.txt delete mode 100644 kode_eksport_1/backend_scrape_status_py.txt delete mode 100644 kode_eksport_1/backend_scrape_vtg_py.txt delete mode 100644 kode_eksport_1/backend_sync_greenfee_py.txt delete mode 100644 kode_eksport_1/backend_test_gemini_py.txt delete mode 100644 kode_eksport_1/backend_test_login_py.txt delete mode 100644 kode_eksport_1/backend_update_admin_py.txt delete mode 100644 kode_eksport_1/eksport_script_py.txt delete mode 100644 kode_eksport_1/frontend_next-env_d_ts.txt delete mode 100644 kode_eksport_1/frontend_next_config_ts.txt delete mode 100644 kode_eksport_1/frontend_src_app_FacilitySearch_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_HeroSlider_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_admin_greenfee_page_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_admin_login_page_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_admin_medlemskap_page_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_admin_page_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_admin_rediger_[slug]_EditFacilityClient_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_admin_rediger_[slug]_page_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_admin_vtg_page_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_golfbaner_[slug]_CourseDisplay_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_golfbaner_[slug]_FacilityDetailView_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_golfbaner_[slug]_page_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_layout_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_app_page_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_components_Header_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_components_ScrapeMethodSelect_tsx.txt delete mode 100644 kode_eksport_1/frontend_src_config_constants_ts.txt delete mode 100644 kode_eksport_1/frontend_src_middleware_ts.txt delete mode 100644 kode_eksport_3/backend_create_admin_py.txt delete mode 100644 kode_eksport_3/backend_import_gallery_py.txt delete mode 100644 kode_eksport_3/backend_import_nye_felter_py.txt delete mode 100644 kode_eksport_3/backend_import_urls_py.txt delete mode 100644 kode_eksport_3/backend_import_wp_py.txt delete mode 100644 kode_eksport_3/backend_main_py.txt delete mode 100644 kode_eksport_3/backend_scrape_golfamore1_3_py.txt delete mode 100644 kode_eksport_3/backend_scrape_greenfee_py.txt delete mode 100644 kode_eksport_3/backend_scrape_membership_py.txt delete mode 100644 kode_eksport_3/backend_scrape_nsg_3_py.txt delete mode 100644 kode_eksport_3/backend_scrape_status_py.txt delete mode 100644 kode_eksport_3/backend_scrape_vtg_py.txt delete mode 100644 kode_eksport_3/backend_sync_greenfee_py.txt delete mode 100644 kode_eksport_3/backend_test_gemini_py.txt delete mode 100644 kode_eksport_3/backend_test_login_py.txt delete mode 100644 kode_eksport_3/backend_update_admin_py.txt delete mode 100644 kode_eksport_3/eksport_script_py.txt delete mode 100644 kode_eksport_3/frontend_next-env_d_ts.txt delete mode 100644 kode_eksport_3/frontend_next_config_ts.txt delete mode 100644 kode_eksport_3/frontend_src_app_FacilitySearch_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_HeroSlider_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_admin_greenfee_page_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_admin_login_page_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_admin_medlemskap_page_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_admin_page_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_admin_rediger_[slug]_EditFacilityClient_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_admin_rediger_[slug]_page_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_admin_vtg_page_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_golfbaner_[slug]_CourseDisplay_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_golfbaner_[slug]_FacilityDetailView_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_golfbaner_[slug]_page_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_layout_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_app_page_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_components_Header_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_components_ScrapeMethodSelect_tsx.txt delete mode 100644 kode_eksport_3/frontend_src_config_constants_ts.txt delete mode 100644 kode_eksport_3/frontend_src_middleware_ts.txt delete mode 100644 struktur2_dump.txt delete mode 100644 struktur3_dump.txt diff --git a/.gitignore b/.gitignore index 6420ef0..2a4295b 100755 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ backend/.env *.dump *_dump.txt kode_eksport_*/ +fil-tre*.txt +frontend/src/struktur_dump.txt diff --git a/fil-tre-3.txt b/fil-tre-3.txt deleted file mode 100644 index 16beb71..0000000 --- a/fil-tre-3.txt +++ /dev/null @@ -1,796 +0,0 @@ - πŸ“ teeoff/ - πŸ“„ fil-tre.txt - πŸ“„ struktur2_dump.txt - πŸ“„ seed.sql - πŸ“„ struktur3_dump.txt - πŸ“„ eksport_script.py - πŸ“„ update_golfbox.sql - πŸ“„ fil-tre-3.txt - πŸ“„ docker-compose.yml - πŸ“„ schema.sql - πŸ“„ init.sql -πŸ“ kode_eksport_3/ - πŸ“„ backend_scrape_membership_py.txt - πŸ“„ backend_scrape_greenfee_py.txt - πŸ“„ frontend_src_components_Header_tsx.txt - πŸ“„ backend_scrape_nsg_3_py.txt - πŸ“„ frontend_src_app_admin_medlemskap_page_tsx.txt - πŸ“„ frontend_next-env_d_ts.txt - πŸ“„ frontend_src_app_layout_tsx.txt - πŸ“„ frontend_src_app_admin_vtg_page_tsx.txt - πŸ“„ frontend_src_app_admin_greenfee_page_tsx.txt - πŸ“„ backend_import_urls_py.txt - πŸ“„ frontend_src_app_page_tsx.txt - πŸ“„ eksport_script_py.txt - πŸ“„ frontend_src_components_ScrapeMethodSelect_tsx.txt - πŸ“„ frontend_src_app_golfbaner_[slug]_page_tsx.txt - πŸ“„ backend_import_wp_py.txt - πŸ“„ frontend_src_middleware_ts.txt - πŸ“„ backend_test_gemini_py.txt - πŸ“„ frontend_src_app_golfbaner_[slug]_CourseDisplay_tsx.txt - πŸ“„ frontend_next_config_ts.txt - πŸ“„ backend_update_admin_py.txt - πŸ“„ backend_import_nye_felter_py.txt - πŸ“„ frontend_src_app_admin_login_page_tsx.txt - πŸ“„ frontend_src_app_golfbaner_[slug]_FacilityDetailView_tsx.txt - πŸ“„ backend_main_py.txt - πŸ“„ frontend_src_app_admin_rediger_[slug]_page_tsx.txt - πŸ“„ frontend_src_app_admin_page_tsx.txt - πŸ“„ frontend_src_app_admin_rediger_[slug]_EditFacilityClient_tsx.txt - πŸ“„ frontend_src_app_HeroSlider_tsx.txt - πŸ“„ backend_test_login_py.txt - πŸ“„ backend_create_admin_py.txt - πŸ“„ backend_sync_greenfee_py.txt - πŸ“„ backend_scrape_status_py.txt - πŸ“„ backend_scrape_golfamore1_3_py.txt - πŸ“„ frontend_src_app_FacilitySearch_tsx.txt - πŸ“„ backend_scrape_vtg_py.txt - πŸ“„ frontend_src_config_constants_ts.txt - πŸ“„ backend_import_gallery_py.txt -πŸ“ frontend/ - πŸ“„ eslint.config.mjs - πŸ“„ next-env.d.ts - πŸ“„ tsconfig.json - πŸ“„ README.md - πŸ“„ next.config.ts - πŸ“„ postcss.config.mjs - πŸ“„ package-lock.json - πŸ“„ .gitignore - πŸ“„ package.json - πŸ“„ Dockerfile - πŸ“ public/ - πŸ“„ globe.svg - πŸ“„ vercel.svg - πŸ“„ Toppbilde-standard.jpg - πŸ“„ TeeOff-logo-Retina-1.png - πŸ“„ window.svg - πŸ“„ next.svg - πŸ“„ file.svg - πŸ“ media/ - πŸ“„ slide_naeroysund-golfklubb_5.jpg - πŸ“„ main_hakadal-golfklubb.jpg - πŸ“„ slide_baerum-golfklubb_1.jpg - πŸ“„ slide_egersund-golfklubb_7.jpg - πŸ“„ main_hammerfest-og-kvalsund-golfklubb.jpg - πŸ“„ main_odda-golfklubb.jpg - πŸ“„ slide_holtsmark-golfklubb_1.jpg - πŸ“„ main_voss-golfklubb.jpeg - πŸ“„ slide_soon-golfklubb_2.jpg - πŸ“„ logo_ogna-golfklubb.jpg - πŸ“„ slide_tyrifjord-golfklubb_4.jpg - πŸ“„ slide_tjome-golfklubb_2.jpg - πŸ“„ logo_garder-golfklubb.jpg - πŸ“„ slide_helgeland-golfklubb_1.jpg - πŸ“„ slide_kvinnherad-golfklubb_0.jpg - πŸ“„ main_rauma-golfklubb.jpg - πŸ“„ slide_naeroysund-golfklubb_0.jpg - πŸ“„ slide_hallingdal-golfklubb_1.jpeg - πŸ“„ slide_hakadal-golfklubb_13.jpg - πŸ“„ slide_sunndal-golfklubb_2.jpg - πŸ“„ logo_skjeberg-golfklubb.jpg - πŸ“„ slide_larvik-golfklubb_0.jpg - πŸ“„ logo_tingvoll-golfklubb.jpg - πŸ“„ slide_sandefjord-golfklubb_4.jpg - πŸ“„ logo_onsoy-golfklubb.jpg - πŸ“„ logo_oppdal-golfklubb.jpg - πŸ“„ main_nordhaug-golfklubb.jpg - πŸ“„ main_hof-golfklubb.jpg - πŸ“„ main_hauger-golfklubb.jpg - πŸ“„ slide_vradal-golfklubb_1.jpg - πŸ“„ slide_holtsmark-golfklubb_5.jpg - πŸ“„ main_baerum-golfklubb.jpg - πŸ“„ logo_huseby-hanko-golfklubb.png - πŸ“„ slide_solum-golfklubb_4.jpg - πŸ“„ logo_hardanger-golfklubb.png - πŸ“„ logo_kristiansund-og-omegn-golfklubb.jpg - πŸ“„ main_byneset-golf.jpg - πŸ“„ logo_laerdal-golfklubb.jpg - πŸ“„ slide_kjekstad-golfklubb_0.jpg - πŸ“„ main_namdal-golfklubb.jpg - πŸ“„ main_hallingdal-golfklubb.jpg - πŸ“„ logo_ibestad-golfklubb.jpg - πŸ“„ logo_alta-golfklubb.png - πŸ“„ slide_norsjo-golfklubb_4.jpg - πŸ“„ main_elverum-golfklubb.jpg - πŸ“„ logo_hammerfest-og-kvalsund-golfklubb.jpg - πŸ“„ main_garder-golfklubb.jpg - πŸ“„ logo_lommedalen-golfklubb.jpg - πŸ“„ slide_ski-golfklubb_1.jpg - πŸ“„ logo_gronmo-golfklubb.jpg - πŸ“„ logo_hvam-golfklubb.jpg - πŸ“„ main_nesbyen-golfklubb.jpg - πŸ“„ slide_naeroysund-golfklubb_2.jpg - πŸ“„ slide_tjome-golfklubb_0.jpg - πŸ“„ logo_miklagard-golfklubb.png - πŸ“„ main_nordfjord-golfklubb.jpg - πŸ“„ slide_bjaavann-golfklubb_1.jpg - πŸ“„ main_jaeren-golfklubb.jpg - πŸ“„ slide_salten-golfklubb-bodo-golfpark_1.jpg - πŸ“„ slide_larvik-golfklubb_1.jpg - πŸ“„ main_modum-golfklubb.jpg - πŸ“„ logo_volda-golfklubb.jpg - πŸ“„ main_grini-golfklubb.jpg - πŸ“„ slide_aurskog-golfpark_0.jpg - πŸ“„ main_miklagard-golfklubb.jpg - πŸ“„ main_bjaavann-golfklubb.jpg - πŸ“„ main_fet-golfklubb.jpg - πŸ“„ slide_atlungstad-golfklubb_2.jpg - πŸ“„ logo_kvinnherad-golfklubb.jpg - πŸ“„ slide_borregaard-golfklubb_1.jpg - πŸ“„ slide_tjome-golfklubb_4.jpg - πŸ“„ main_rjukan-og-tinn-golfklubb.jpg - πŸ“„ slide_kjekstad-golfklubb_1.jpg - πŸ“„ slide_norsjo-golfklubb_3.jpg - πŸ“„ logo_surnadal-golfklubb.png - πŸ“„ slide_kongsberg-golfklubb_0.jpg - πŸ“„ slide_hovden-golfklubb_1.jpg - πŸ“„ main_hemsedal-golfklubb.jpg - πŸ“„ slide_tyrifjord-golfklubb_5.jpg - πŸ“„ logo_alesund-golfklubb.jpg - πŸ“„ slide_tingvoll-golfklubb_0.jpg - πŸ“„ slide_tyrifjord-golfklubb_2.jpg - πŸ“„ slide_bleik-golfstrombane_0.jpg - πŸ“„ slide_nesbyen-golfklubb_0.jpg - πŸ“„ main_sunnfjord-golfklubb.jpg - πŸ“„ slide_hasvik-golfklubb_0.jpg - πŸ“„ logo_stavanger-golfklubb.png - πŸ“„ slide_ostmarka-golfklubb_3.jpg - πŸ“„ main_land-golfklubb.jpg - πŸ“„ slide_helgeland-golfklubb_5.jpg - πŸ“„ logo_bergen-golfklubb.png - πŸ“„ slide_lofoten-golfklubb_1.jpg - πŸ“„ logo_rjukan-og-tinn-golfklubb.jpg - πŸ“„ main_alta-golfklubb.jpg - πŸ“„ slide_asker-golfklubb_1.jpg - πŸ“„ slide_krokhol-golfklubb_5.jpg - πŸ“„ logo_nordvegen-golfklubb.png - πŸ“„ logo_kristiansand-golfklubb.jpg - πŸ“„ main_asker-golfklubb.jpg - πŸ“„ slide_sleneset-golfklubb_0.jpg - πŸ“„ logo_re-golfklubb.jpg - πŸ“„ slide_oustoen-country-club_0.jpg - πŸ“„ logo_hallingdal-golfklubb.png - πŸ“„ logo_fet-golfklubb.jpg - πŸ“„ slide_trondheim-golfklubb_3.jpg - πŸ“„ slide_borregaard-golfklubb_3.jpg - πŸ“„ main_hovden-golfklubb.jpg - πŸ“„ slide_ostmarka-golfklubb_2.jpg - πŸ“„ slide_sandane-golfklubb_2.jpg - πŸ“„ slide_trondheim-golfklubb_6.jpg - πŸ“„ slide_helgeland-golfklubb_9.jpg - πŸ“„ logo_borre-golfklubb.png - πŸ“„ main_karasjok-golfklubb.jpg - πŸ“„ slide_hallingdal-golfklubb_2.jpeg - πŸ“„ main_north-cape-golf-club.jpg - πŸ“„ logo_selbu-golfklubb.png - πŸ“„ logo_sola-golfklubb-forus.jpg - πŸ“„ slide_asker-golfklubb_0.jpg - πŸ“„ main_hafjell-golfklubb.jpg - πŸ“„ logo_mjosen-golfklubb.png - πŸ“„ slide_atlungstad-golfklubb_0.jpg - πŸ“„ main_moa-golfsenter.jpg - πŸ“„ slide_bleik-golfstrombane_7.jpg - πŸ“„ main_kvinnherad-golfklubb.jpg - πŸ“„ main_re-golfklubb.jpg - πŸ“„ logo_haga-golfklubb.jpg - πŸ“„ main_roros-golfklubb.jpg - πŸ“„ main_vestfold-golfklubb.jpg - πŸ“„ slide_egersund-golfklubb_2.jpg - πŸ“„ slide_volda-golfklubb_2.jpeg - πŸ“„ slide_egersund-golfklubb_5.jpg - πŸ“„ main_gjersjoen-golfklubb.jpg - πŸ“„ main_gamle-fredrikstad-golfklubb.jpg - πŸ“„ slide_naeroysund-golfklubb_3.jpg - πŸ“„ slide_oustoen-country-club_1.jpg - πŸ“„ logo_hitra-golfklubb.png - πŸ“„ logo_rygge-flystasjon-golf-klubb.jpg - πŸ“„ slide_tyrifjord-golfklubb_9.jpg - πŸ“„ slide_trondheim-golfklubb_5.jpg - πŸ“„ slide_aurskog-golfpark_1.jpg - πŸ“„ main_sola-golfklubb-solastranden.jpg - πŸ“„ logo_trysil-golfklubb.jpg - πŸ“„ logo_harstad-golfklubb.jpg - πŸ“„ slide_ogna-golfklubb_1.jpg - πŸ“„ logo_gamle-fredrikstad-golfklubb.jpg - πŸ“„ main_stranda-golfklubb.jpg - πŸ“„ logo_sauda-golfklubb.png - πŸ“„ slide_egersund-golfklubb_0.jpg - πŸ“„ logo_solum-golfklubb.jpg - πŸ“„ main_bjornefjorden-golfklubb.jpg - πŸ“„ main_vestlia-golf.jpg - πŸ“„ slide_sandefjord-golfklubb_1.jpg - πŸ“„ logo_borregaard-golfklubb.jpg - πŸ“„ main_grimstad-golfklubb.jpg - πŸ“„ logo_vesteralen-golfklubb.png - πŸ“„ main_kristiansund-og-omegn-golfklubb.jpg - πŸ“„ slide_hallingdal-golfklubb_3.jpeg - πŸ“„ logo_sandnes-golfklubb.jpg - πŸ“„ main_alesund-golfklubb.jpeg - πŸ“„ main_steinkjer-golfklubb.jpg - πŸ“„ main_skei-golfklubb.jpg - πŸ“„ slide_nesbyen-golfklubb_3.jpg - πŸ“„ slide_hardanger-golfklubb_0.jpg - πŸ“„ slide_gumoy-golf_1.jpg - πŸ“„ main_grenland-og-omegn-golfklubb.jpg - πŸ“„ main_karmoy-golfklubb.jpg - πŸ“„ logo_egersund-golfklubb.jpg - πŸ“„ logo_baerum-golfklubb.png - πŸ“„ main_kristiansand-golfklubb.jpg - πŸ“„ slide_kongsberg-golfklubb_2.jpg - πŸ“„ slide_gumoy-golf_3.jpg - πŸ“„ logo_smola-golfklubb.jpg - πŸ“„ logo_grimstad-golfklubb.png - πŸ“„ logo_stord-golfklubb.png - πŸ“„ slide_helgeland-golfklubb_6.jpg - πŸ“„ logo_rauma-golfklubb.png - πŸ“„ logo_hinnoy-golfklubb.jpg - πŸ“„ slide_holtsmark-golfklubb_0.jpg - πŸ“„ logo_varanger-golfklubb.jpg - πŸ“„ logo_mandal-golfklubb.jpg - πŸ“„ slide_trondheim-golfklubb_1.jpg - πŸ“„ logo_tjome-golfklubb.png - πŸ“„ slide_lofoten-golfklubb_2.jpg - πŸ“„ main_haga-golfklubb.jpg - πŸ“„ slide_bjaavann-golfklubb_2.jpg - πŸ“„ slide_hakadal-golfklubb_8.jpg - πŸ“„ logo_herdla-golfklubb.jpg - πŸ“„ main_stavanger-golfklubb.jpg - πŸ“„ main_lommedalen-golfklubb.jpg - πŸ“„ main_sirdal-fjellgolf-klubb.jpg - πŸ“„ main_alsten-golfklubb.jpg - πŸ“„ logo_lillestrom-golfklubb.png - πŸ“„ main_ogna-golfklubb.jpg - πŸ“„ slide_helgeland-golfklubb_8.jpg - πŸ“„ slide_nesbyen-golfklubb_2.jpg - πŸ“„ main_tjome-golfklubb.jpg - πŸ“„ main_sandefjord-golfklubb.jpg - πŸ“„ main_namsos-golfklubb.jpg - πŸ“„ logo_notteroy-golfklubb.jpg - πŸ“„ main_trondheim-golfklubb.jpg - πŸ“„ slide_solum-golfklubb_1.jpg - πŸ“„ slide_kongsberg-golfklubb_4.jpg - πŸ“„ main_utsikten-golfklubb.jpg - πŸ“„ main_vesteralen-golfklubb.jpg - πŸ“„ slide_sandefjord-golfklubb_0.jpg - πŸ“„ slide_hakadal-golfklubb_9.jpg - πŸ“„ logo_polarsirkelen-golfklubb.jpg - πŸ“„ logo_sandane-golfklubb.jpg - πŸ“„ slide_groruddalen-golfklubb_2.jpg - πŸ“„ main_ski-golfklubb.jpg - πŸ“„ logo_bjornefjorden-golfklubb.png - πŸ“„ main_polarsirkelen-golfklubb.jpg - πŸ“„ slide_krokhol-golfklubb_1.jpg - πŸ“„ main_groruddalen-golfklubb.jpg - πŸ“„ slide_krokhol-golfklubb_4.jpg - πŸ“„ main_vanylven-golfklubb.jpg - πŸ“„ main_borregaard-golfklubb.jpg - πŸ“„ slide_sandefjord-golfklubb_5.jpg - πŸ“„ slide_helgeland-golfklubb_2.jpg - πŸ“„ main_hinnoy-golfklubb.jpg - πŸ“„ slide_sunndal-golfklubb_0.jpg - πŸ“„ logo_groruddalen-golfklubb.png - πŸ“„ main_tingvoll-golfklubb.jpg - πŸ“„ slide_tjome-golfklubb_3.jpg - πŸ“„ main_lindesnes-golfklubb.jpg - πŸ“„ slide_kongsvingers-golfklubb_5.jpg - πŸ“„ slide_tromso-golfklubb_0.jpg - πŸ“„ main_randaberg-golfklubb.jpg - πŸ“„ logo_krokhol-golfklubb.png - πŸ“„ main_smola-golfklubb.jpg - πŸ“„ logo_sande-golfklubb.jpg - πŸ“„ logo_oslo-golfklubb.jpg - πŸ“„ logo_preikestolen.jpg - πŸ“„ logo_sunnfjord-golfklubb.jpg - πŸ“„ slide_lofoten-golfklubb_3.jpg - πŸ“„ main_bleik-golfstrombane.jpg - πŸ“„ slide_gumoy-golf_0.jpg - πŸ“„ logo_modum-golfklubb.png - πŸ“„ slide_tingvoll-golfklubb_1.jpg - πŸ“„ logo_bodo-golfklubb.jpg - πŸ“„ logo_nesbyen-golfklubb.png - πŸ“„ main_selbu-golfklubb.jpg - πŸ“„ slide_holtsmark-golfklubb_2.jpg - πŸ“„ logo_imjelt-pitch-putt.jpg - πŸ“„ logo_ullensaker-golfklubb.png - πŸ“„ slide_elverum-golfklubb_1.jpg - πŸ“„ logo_ringerike-golfklubb.jpg - πŸ“„ slide_hakadal-golfklubb_6.jpg - πŸ“„ slide_volda-golfklubb_4.jpg - πŸ“„ slide_tyrifjord-golfklubb_6.jpg - πŸ“„ main_solum-golfklubb.jpg - πŸ“„ slide_helgeland-golfklubb_4.jpg - πŸ“„ main_stiklestad-golfklubb.jpg - πŸ“„ slide_stavanger-golfklubb_0.jpg - πŸ“„ logo_vestlia-golf.jpg - πŸ“„ slide_vestfold-golfklubb_0.jpg - πŸ“„ logo_floro-golfklubb.jpg - πŸ“„ logo_voss-golfklubb.jpg - πŸ“„ slide_tyrifjord-golfklubb_8.jpg - πŸ“„ slide_sotra-golfklubb_2.jpg - πŸ“„ logo_jaeren-golfklubb.png - πŸ“„ slide_moss-rygge-golfklubb_0.jpg - πŸ“„ logo_asker-golfklubb.png - πŸ“„ slide_bleik-golfstrombane_6.jpg - πŸ“„ slide_bjaavann-golfklubb_3.jpg - πŸ“„ main_hvam-golfklubb.jpg - πŸ“„ logo_eiker-golfklubb.jpg - πŸ“„ slide_bleik-golfstrombane_9.jpg - πŸ“„ slide_namdal-golfklubb_0.jpg - πŸ“„ logo_nordhaug-golfklubb.jpg - πŸ“„ slide_borregaard-golfklubb_0.jpg - πŸ“„ slide_kongsberg-golfklubb_5.jpg - πŸ“„ main_hardanger-golfklubb.jpg - πŸ“„ slide_ostmarka-golfklubb_5.jpg - πŸ“„ logo_oppegard-golfklubb.png - πŸ“„ slide_vradal-golfklubb_0.jpg - πŸ“„ main_narvik-golfklubb.jpg - πŸ“„ logo_midt-troms-golfklubb.jpg - πŸ“„ slide_solum-golfklubb_2.jpg - πŸ“„ slide_sunndal-golfklubb_1.jpg - πŸ“„ slide_gjersjoen-golfklubb_1.jpg - πŸ“„ slide_preikestolen_0.jpg - πŸ“„ main_huseby-hanko-golfklubb.jpg - πŸ“„ slide_holtsmark-golfklubb_4.jpg - πŸ“„ main_larvik-golfklubb.jpg - πŸ“„ slide_hasvik-golfklubb_1.jpg - πŸ“„ slide_herdla-golfklubb_1.jpg - πŸ“„ logo_kjekstad-golfklubb.jpg - πŸ“„ main_austratt-golfklubb.jpg - πŸ“„ slide_sandefjord-golfklubb_3.jpg - πŸ“„ logo_hakadal-golfklubb.png - πŸ“„ slide_bleik-golfstrombane_3.jpg - πŸ“„ logo_vanylven-golfklubb.jpg - πŸ“„ main_skjeberg-golfklubb.jpg - πŸ“„ slide_hovden-golfklubb_3.jpg - πŸ“„ slide_ostmarka-golfklubb_0.jpg - πŸ“„ logo_tromso-golfklubb.png - πŸ“„ main_stjordal-golfklubb.jpg - πŸ“„ slide_frosta-golfklubb_3.jpg - πŸ“„ main_nordvegen-golfklubb.jpg - πŸ“„ slide_norsjo-golfklubb_1.jpg - πŸ“„ logo_gumoy-golf.png - πŸ“„ slide_gronmo-golfklubb_2.jpg - πŸ“„ slide_hardanger-golfklubb_1.jpg - πŸ“„ main_oppdal-golfklubb.jpg - πŸ“„ slide_trondheim-golfklubb_0.jpg - πŸ“„ main_lillestrom-golfklubb.jpg - πŸ“„ slide_gjersjoen-golfklubb_0.jpg - πŸ“„ slide_soon-golfklubb_1.jpg - πŸ“„ logo_vestfold-golfklubb.jpg - πŸ“„ slide_stavanger-golfklubb_1.jpg - πŸ“„ main_norefjell-golfklubb.jpg - πŸ“„ main_kongsberg-golfklubb.jpg - πŸ“„ slide_gjovik-og-toten-golfklubb_2.jpg - πŸ“„ logo_veierland-golfklubb.jpg - πŸ“„ slide_soon-golfklubb_0.jpg - πŸ“„ slide_egersund-golfklubb_6.jpg - πŸ“„ slide_norsjo-golfklubb_0.jpg - πŸ“„ slide_kristiansand-golfklubb_0.jpg - πŸ“„ main_hurum-golfklubb.jpg - πŸ“„ main_sola-golfklubb-forus.jpg - πŸ“„ logo_mork-golfklubb.jpg - πŸ“„ logo_molde-golfklubb.png - πŸ“„ main_kongsvingers-golfklubb.jpg - πŸ“„ main_sunndal-golfklubb.jpg - πŸ“„ logo_utsikten-golfklubb.jpg - πŸ“„ logo_austratt-golfklubb.jpg - πŸ“„ logo_selje-golfklubb.jpg - πŸ“„ main_stord-golfklubb.jpg - πŸ“„ logo_sorknes-golfklubb.jpg - πŸ“„ slide_krokhol-golfklubb_3.jpg - πŸ“„ slide_hovden-golfklubb_4.jpg - πŸ“„ main_preikestolen.jpg - πŸ“„ logo_aurskog-golfpark.png - πŸ“„ main_aurskog-golfpark.jpg - πŸ“„ slide_mork-golfklubb_1.jpg - πŸ“„ logo_roros-golfklubb.jpg - πŸ“„ main_harstad-golfklubb.jpg - πŸ“„ main_helgeland-golfklubb.jpg - πŸ“„ slide_ogna-golfklubb_0.jpg - πŸ“„ slide_kvinnherad-golfklubb_1.jpg - πŸ“„ logo_haugesund-golfklubb.png - πŸ“„ slide_bjaavann-golfklubb_0.jpg - πŸ“„ slide_hakadal-golfklubb_3.jpg - πŸ“„ slide_ostmarka-golfklubb_6.jpg - πŸ“„ slide_bleik-golfstrombane_11.jpg - πŸ“„ logo_moss-rygge-golfklubb.png - πŸ“„ slide_borregaard-golfklubb_2.jpg - πŸ“„ main_lofoten-golfklubb.jpg - πŸ“„ slide_bleik-golfstrombane_5.jpg - πŸ“„ logo_oustoen-country-club.jpg - πŸ“„ main_oslo-golfklubb.jpg - πŸ“„ slide_gjovik-og-toten-golfklubb_0.jpg - πŸ“„ slide_frosta-golfklubb_1.jpg - πŸ“„ slide_ostmarka-golfklubb_7.jpg - πŸ“„ slide_helgeland-golfklubb_3.jpg - πŸ“„ logo_vradal-golfklubb.png - πŸ“„ main_salten-golfklubb-bodo-golfpark.jpg - πŸ“„ main_rygge-flystasjon-golf-klubb.jpg - πŸ“„ slide_trondheim-golfklubb_4.jpg - πŸ“„ main_meland-golfklubb.jpg - πŸ“„ slide_sotra-golfklubb_1.jpg - πŸ“„ main_sandane-golfklubb.jpg - πŸ“„ slide_baerum-golfklubb_0.jpg - πŸ“„ slide_egersund-golfklubb_3.jpg - πŸ“„ slide_frosta-golfklubb_5.jpg - πŸ“„ logo_sandefjord-golfklubb.png - πŸ“„ slide_egersund-golfklubb_4.jpg - πŸ“„ slide_sandane-golfklubb_1.jpg - πŸ“„ slide_bleik-golfstrombane_2.jpg - πŸ“„ main_eiker-golfklubb.jpg - πŸ“„ main_tromso-golfklubb.jpg - πŸ“„ logo_tysnes-golfklubb.jpg - πŸ“„ slide_hallingdal-golfklubb_4.jpeg - πŸ“„ main_ringerike-golfklubb.jpg - πŸ“„ main_norsjo-golfklubb.jpg - πŸ“„ logo_byneset-golf.jpg - πŸ“„ logo_land-golfklubb.png - πŸ“„ slide_bleik-golfstrombane_8.jpg - πŸ“„ logo_losby-golfklubb.png - πŸ“„ logo_husoy-golfklubb.png - πŸ“„ main_nittedal-golfklubb.jpg - πŸ“„ logo_trondheim-par3golf-havstein.png - πŸ“„ main_mork-golfklubb.jpg - πŸ“„ slide_sleneset-golfklubb_1.jpg - πŸ“„ logo_ekholtbruket-golfklubb.jpg - πŸ“„ slide_hauger-golfklubb_1.jpg - πŸ“„ main_borre-golfklubb.jpg - πŸ“„ main_kragero-golfklubb.jpg - πŸ“„ main_lonne-golfklubb.jpg - πŸ“„ logo_namsos-golfklubb.jpg - πŸ“„ slide_hallingdal-golfklubb_0.jpeg - πŸ“„ slide_solum-golfklubb_0.jpg - πŸ“„ main_holtsmark-golfklubb.jpg - πŸ“„ main_oustoen-country-club.jpg - πŸ“„ slide_hakadal-golfklubb_12.jpg - πŸ“„ slide_gronmo-golfklubb_1.jpg - πŸ“„ main_mjosen-golfklubb.jpg - πŸ“„ logo_sunndal-golfklubb.jpg - πŸ“„ logo_larvik-golfklubb.jpg - πŸ“„ slide_ekholtbruket-golfklubb_0.jpg - πŸ“„ main_husoy-golfklubb.jpg - πŸ“„ logo_drammen-golfklubb.png - πŸ“„ logo_helgeland-golfklubb.png - πŸ“„ main_gjerdrum-golfklubb.jpg - πŸ“„ slide_sandefjord-golfklubb_2.jpg - πŸ“„ main_hvaler-golfklubb.jpg - πŸ“„ logo_sunnmore-golfklubb.jpg - πŸ“„ slide_hauger-golfklubb_0.jpg - πŸ“„ main_naeroysund-golfklubb.jpg - πŸ“„ slide_hakadal-golfklubb_5.jpg - πŸ“„ logo_bamble-golfklubb.png - πŸ“„ slide_sandane-golfklubb_0.jpg - πŸ“„ main_bodo-golfklubb.jpeg - πŸ“„ main_drammen-golfklubb.jpg - πŸ“„ slide_frosta-golfklubb_4.jpg - πŸ“„ slide_solum-golfklubb_3.jpg - πŸ“„ slide_bleik-golfstrombane_1.jpg - πŸ“„ main_trondheim-par3golf-havstein.jpg - πŸ“„ logo_kongsvingers-golfklubb.jpg - πŸ“„ slide_volda-golfklubb_3.jpeg - πŸ“„ slide_tyrifjord-golfklubb_3.jpg - πŸ“„ slide_hakadal-golfklubb_2.jpg - πŸ“„ slide_hovden-golfklubb_2.jpg - πŸ“„ slide_kongsvingers-golfklubb_4.jpg - πŸ“„ main_arendal-omegn-golfklubb.jpg - πŸ“„ logo_bjaavann-golfklubb.png - πŸ“„ main_sande-golfklubb.jpg - πŸ“„ main_ballerud-golfklubb.jpg - πŸ“„ logo_norefjell-golfklubb.png - πŸ“„ slide_kristiansand-golfklubb_1.jpg - πŸ“„ main_veierland-golfklubb.jpg - πŸ“„ logo_karmoy-golfklubb.png - πŸ“„ logo_lofoten-golfklubb.png - πŸ“„ slide_lofoten-golfklubb_0.jpg - πŸ“„ logo_hurum-golfklubb.png - πŸ“„ slide_holtsmark-golfklubb_3.jpg - πŸ“„ logo_hemsedal-golfklubb.jpg - πŸ“„ slide_borregaard-golfklubb_5.jpg - πŸ“„ logo_haugaland-golfklubb.png - πŸ“„ slide_bjornefjorden-golfklubb_1.jpg - πŸ“„ main_notteroy-golfklubb.jpg - πŸ“„ slide_kongsvingers-golfklubb_3.jpg - πŸ“„ slide_kongsvingers-golfklubb_2.jpg - πŸ“„ main_fana-golfklubb.jpg - πŸ“„ logo_giske-golfklubb.jpg - πŸ“„ slide_hakadal-golfklubb_11.jpg - πŸ“„ main_nes-golfklubb-09.jpg - πŸ“„ main_volda-golfklubb.jpeg - πŸ“„ logo_arendal-omegn-golfklubb.png - πŸ“„ slide_gjovik-og-toten-golfklubb_1.jpg - πŸ“„ main_soon-golfklubb.jpg - πŸ“„ logo_soon-golfklubb.png - πŸ“„ slide_gumoy-golf_2.jpg - πŸ“„ logo_alsten-golfklubb.png - πŸ“„ main_halden-golfklubb.jpg - πŸ“„ logo_narvik-golfklubb.jpg - πŸ“„ logo_randaberg-golfklubb.jpg - πŸ“„ slide_hakadal-golfklubb_10.jpg - πŸ“„ main_ostmarka-golfklubb.jpg - πŸ“„ logo_bleik-golfstrombane.jpg - πŸ“„ logo_hasvik-golfklubb.png - πŸ“„ main_floro-golfklubb.jpg - πŸ“„ main_gronmo-golfklubb.jpg - πŸ“„ slide_hakadal-golfklubb_7.jpg - πŸ“„ slide_egersund-golfklubb_8.jpg - πŸ“„ logo_atlungstad-golfklubb.png - πŸ“„ main_sunnmore-golfklubb.jpeg - πŸ“„ main_sleneset-golfklubb.jpg - πŸ“„ logo_lonne-golfklubb.png - πŸ“„ main_haugaland-golfklubb.jpg - πŸ“„ main_egersund-golfklubb.jpg - πŸ“„ slide_solum-golfklubb_5.jpg - πŸ“„ slide_gamle-fredrikstad-golfklubb_1.jpg - πŸ“„ main_sotra-golfklubb.jpg - πŸ“„ main_trysil-golfklubb.jpg - πŸ“„ logo_kongsberg-golfklubb.png - πŸ“„ slide_tromso-golfklubb_1.jpg - πŸ“„ logo_frosta-golfklubb.jpg - πŸ“„ slide_groruddalen-golfklubb_1.jpg - πŸ“„ logo_hafjell-golfklubb.png - πŸ“„ logo_sola-golfklubb-solastranden.png - πŸ“„ logo_holtsmark-golfklubb.png - πŸ“„ slide_naeroysund-golfklubb_1.jpg - πŸ“„ slide_groruddalen-golfklubb_0.jpg - πŸ“„ slide_naeroysund-golfklubb_4.jpg - πŸ“„ slide_kjekstad-golfklubb_2.jpg - πŸ“„ slide_gronmo-golfklubb_0.jpg - πŸ“„ slide_preikestolen_1.jpg - πŸ“„ logo_fana-golfklubb.jpg - πŸ“„ slide_sorknes-golfklubb_0.jpg - πŸ“„ slide_atlungstad-golfklubb_3.jpg - πŸ“„ main_losby-golfklubb.jpg - πŸ“„ slide_borregaard-golfklubb_4.jpg - πŸ“„ slide_hakadal-golfklubb_1.jpg - πŸ“„ main_sandnes-golfklubb.jpg - πŸ“„ slide_norsjo-golfklubb_2.jpg - πŸ“„ main_frosta-golfklubb.jpg - πŸ“„ slide_krokhol-golfklubb_0.jpg - πŸ“„ logo_askim-golfklubb.png - πŸ“„ main_stryn-golfklubb.jpg - πŸ“„ slide_lofoten-golfklubb_5.jpg - πŸ“„ logo_karasjok-golfklubb.jpg - πŸ“„ slide_krokhol-golfklubb_2.jpg - πŸ“„ slide_hakadal-golfklubb_14.jpg - πŸ“„ logo_gjovik-og-toten-golfklubb.png - πŸ“„ main_haugesund-golfklubb.jpg - πŸ“„ logo_stjordal-golfklubb.jpg - πŸ“„ logo_drobak-golfklubb.png - πŸ“„ main_atlungstad-golfklubb.jpg - πŸ“„ logo_trondheim-golfklubb.jpg - πŸ“„ logo_gjersjoen-golfklubb.png - πŸ“„ slide_lofoten-golfklubb_4.jpg - πŸ“„ slide_helgeland-golfklubb_7.jpg - πŸ“„ main_molde-golfklubb.jpg - πŸ“„ slide_tyrifjord-golfklubb_7.jpg - πŸ“„ logo_gjerdrum-golfklubb.png - πŸ“„ main_onsoy-golfklubb.jpg - πŸ“„ slide_trondheim-golfklubb_2.jpg - πŸ“„ slide_mork-golfklubb_0.jpg - πŸ“„ slide_frosta-golfklubb_2.jpg - πŸ“„ slide_giske-golfklubb_1.jpeg - πŸ“„ slide_moss-rygge-golfklubb_1.jpg - πŸ“„ slide_ostmarka-golfklubb_1.jpg - πŸ“„ slide_volda-golfklubb_0.jpeg - πŸ“„ logo_sotra-golfklubb.jpg - πŸ“„ main_mandal-golfklubb.jpg - πŸ“„ logo_grenland-og-omegn-golfklubb.jpg - πŸ“„ slide_volda-golfklubb_1.jpeg - πŸ“„ main_imjelt-pitch-putt.jpg - πŸ“„ logo_nittedal-golfklubb.png - πŸ“„ main_gumoy-golf.jpg - πŸ“„ logo_hauger-golfklubb.png - πŸ“„ slide_hakadal-golfklubb_4.jpg - πŸ“„ slide_ostmarka-golfklubb_4.jpg - πŸ“„ slide_hakadal-golfklubb_0.jpg - πŸ“„ main_surnadal-golfklubb.jpg - πŸ“„ logo_hof-golfklubb.png - πŸ“„ slide_sleneset-golfklubb_2.jpg - πŸ“„ logo_halden-golfklubb.png - πŸ“„ logo_naeroysund-golfklubb.png - πŸ“„ logo_elverum-golfklubb.png - πŸ“„ main_bamble-golfklubb.jpg - πŸ“„ logo_kragero-golfklubb.png - πŸ“„ logo_valdres-golfklubb.png - πŸ“„ slide_tyrifjord-golfklubb_1.jpg - πŸ“„ logo_grini-golfklubb.png - πŸ“„ logo_lindesnes-golfklubb.jpg - πŸ“„ slide_lofoten-golfklubb_6.jpg - πŸ“„ main_sauda-golfklubb.jpg - πŸ“„ main_moss-rygge-golfklubb.jpg - πŸ“„ main_dalane-golfklubb.jpg - πŸ“„ logo_skei-golfklubb.jpg - πŸ“„ logo_ostmarka-golfklubb.png - πŸ“„ slide_nesbyen-golfklubb_1.jpg - πŸ“„ logo_stryn-golfklubb.png - πŸ“„ main_vradal-golfklubb.jpg - πŸ“„ slide_hovden-golfklubb_0.jpg - πŸ“„ main_gjovik-og-toten-golfklubb.jpg - πŸ“„ logo_hvaler-golfklubb.png - πŸ“„ main_giske-golfklubb.jpeg - πŸ“„ slide_herdla-golfklubb_0.jpg - πŸ“„ main_ibestad-golfklubb.jpg - πŸ“„ slide_kongsvingers-golfklubb_0.jpg - πŸ“„ slide_atlungstad-golfklubb_1.jpg - πŸ“„ logo_nordfjord-golfklubb.png - πŸ“„ logo_eidskog-golfklubb.png - πŸ“„ main_hitra-golfklubb.jpg - πŸ“„ logo_dalane-golfklubb.jpg - πŸ“„ main_kjekstad-golfklubb.jpg - πŸ“„ logo_odda-golfklubb.jpg - πŸ“„ logo_ballerud-golfklubb.jpg - πŸ“„ main_klaebu-golfklubb.jpg - πŸ“„ main_hasvik-golfklubb.jpg - πŸ“„ logo_klaebu-golfklubb.png - πŸ“„ slide_bleik-golfstrombane_4.jpg - πŸ“„ logo_north-cape-golf-club.jpg - πŸ“„ slide_tyrifjord-golfklubb_0.jpg - πŸ“„ logo_moa-golfsenter.png - πŸ“„ main_herdla-golfklubb.jpg - πŸ“„ slide_gjersjoen-golfklubb_2.jpg - πŸ“„ logo_stiklestad-golfklubb.png - πŸ“„ slide_bleik-golfstrombane_10.jpg - πŸ“„ logo_hovden-golfklubb.jpg - πŸ“„ slide_frosta-golfklubb_0.jpg - πŸ“„ logo_sirdal-fjellgolf-klubb.png - πŸ“„ main_ekholtbruket-golfklubb.jpg - πŸ“„ slide_salten-golfklubb-bodo-golfpark_0.jpg - πŸ“„ main_askim-golfklubb.jpg - πŸ“„ slide_elverum-golfklubb_0.jpg - πŸ“„ slide_gamle-fredrikstad-golfklubb_0.jpg - πŸ“„ main_oppegard-golfklubb.jpg - πŸ“„ slide_sorknes-golfklubb_1.jpg - πŸ“„ main_drobak-golfklubb.jpg - πŸ“„ slide_kongsvingers-golfklubb_1.jpg - πŸ“„ slide_helgeland-golfklubb_0.jpg - πŸ“„ slide_vestfold-golfklubb_1.jpg - πŸ“„ slide_kongsberg-golfklubb_1.jpg - πŸ“„ slide_tjome-golfklubb_1.jpg - πŸ“„ slide_ekholtbruket-golfklubb_1.jpg - πŸ“„ main_bergen-golfklubb.jpg - πŸ“„ logo_salten-golfklubb-bodo-golfpark.jpg - πŸ“„ slide_egersund-golfklubb_1.jpg - πŸ“„ slide_trondheim-golfklubb_7.jpg - πŸ“„ logo_stranda-golfklubb.jpg - πŸ“„ main_eidskog-golfklubb.jpg - πŸ“„ logo_nes-golfklubb-09.jpg - πŸ“„ logo_ski-golfklubb.png - πŸ“„ logo_norsjo-golfklubb.png - πŸ“„ slide_kongsberg-golfklubb_3.jpg - πŸ“„ slide_ski-golfklubb_0.jpg - πŸ“„ main_sorknes-golfklubb.jpg - πŸ“„ main_laerdal-golfklubb.jpg - πŸ“„ slide_giske-golfklubb_0.jpeg - πŸ“„ main_ullensaker-golfklubb.jpg - πŸ“„ main_midt-troms-golfklubb.jpg - πŸ“„ main_tysnes-golfklubb.jpg - πŸ“„ logo_namdal-golfklubb.jpg - πŸ“„ logo_tyrifjord-golfklubb.png - πŸ“„ slide_bjornefjorden-golfklubb_0.jpg - πŸ“„ slide_hovden-golfklubb_5.jpg - πŸ“„ main_tyrifjord-golfklubb.jpg - πŸ“„ slide_sotra-golfklubb_0.jpg - πŸ“„ main_valdres-golfklubb.jpg - πŸ“„ logo_steinkjer-golfklubb.jpg - πŸ“„ main_krokhol-golfklubb.jpg - πŸ“„ main_varanger-golfklubb.jpg - πŸ“„ logo_meland-golfklubb.png - πŸ“„ slide_namdal-golfklubb_1.jpg - πŸ“„ main_selje-golfklubb.jpg - πŸ“ src/ - πŸ“„ struktur_dump.txt - πŸ“„ middleware.ts - πŸ“ components/ - πŸ“„ ScrapeMethodSelect.tsx - πŸ“„ Header.tsx - πŸ“ config/ - πŸ“„ constants.ts - πŸ“ app/ - πŸ“„ FacilitySearch.tsx - πŸ“„ HeroSlider.tsx - πŸ“„ favicon.ico - πŸ“„ globals.css - πŸ“„ page.tsx - πŸ“„ layout.tsx - πŸ“ golfbaner/ - πŸ“ [slug]/ - πŸ“„ CourseDisplay.tsx - πŸ“„ page.tsx - πŸ“„ FacilityDetailView.tsx - πŸ“ admin/ - πŸ“„ page.tsx - πŸ“ rediger/ - πŸ“ [slug]/ - πŸ“„ EditFacilityClient.tsx - πŸ“„ page.tsx - πŸ“ login/ - πŸ“„ page.tsx - πŸ“ vtg/ - πŸ“„ page.tsx - πŸ“ medlemskap/ - πŸ“„ page.tsx - πŸ“ greenfee/ - πŸ“„ page.tsx -πŸ“ kode_eksport_1/ - πŸ“„ backend_scrape_membership_py.txt - πŸ“„ backend_scrape_greenfee_py.txt - πŸ“„ frontend_src_components_Header_tsx.txt - πŸ“„ backend_scrape_nsg_3_py.txt - πŸ“„ frontend_src_app_admin_medlemskap_page_tsx.txt - πŸ“„ frontend_next-env_d_ts.txt - πŸ“„ frontend_src_app_layout_tsx.txt - πŸ“„ frontend_src_app_admin_vtg_page_tsx.txt - πŸ“„ frontend_src_app_admin_greenfee_page_tsx.txt - πŸ“„ backend_import_urls_py.txt - πŸ“„ frontend_src_app_page_tsx.txt - πŸ“„ eksport_script_py.txt - πŸ“„ frontend_src_components_ScrapeMethodSelect_tsx.txt - πŸ“„ frontend_src_app_golfbaner_[slug]_page_tsx.txt - πŸ“„ backend_import_wp_py.txt - πŸ“„ frontend_src_middleware_ts.txt - πŸ“„ backend_test_gemini_py.txt - πŸ“„ frontend_src_app_golfbaner_[slug]_CourseDisplay_tsx.txt - πŸ“„ frontend_next_config_ts.txt - πŸ“„ backend_update_admin_py.txt - πŸ“„ backend_import_nye_felter_py.txt - πŸ“„ frontend_src_app_admin_login_page_tsx.txt - πŸ“„ frontend_src_app_golfbaner_[slug]_FacilityDetailView_tsx.txt - πŸ“„ backend_main_py.txt - πŸ“„ frontend_src_app_admin_rediger_[slug]_page_tsx.txt - πŸ“„ frontend_src_app_admin_page_tsx.txt - πŸ“„ frontend_src_app_admin_rediger_[slug]_EditFacilityClient_tsx.txt - πŸ“„ frontend_src_app_HeroSlider_tsx.txt - πŸ“„ backend_test_login_py.txt - πŸ“„ backend_create_admin_py.txt - πŸ“„ backend_sync_greenfee_py.txt - πŸ“„ backend_scrape_status_py.txt - πŸ“„ backend_scrape_golfamore1_3_py.txt - πŸ“„ frontend_src_app_FacilitySearch_tsx.txt - πŸ“„ backend_scrape_vtg_py.txt - πŸ“„ frontend_src_config_constants_ts.txt - πŸ“„ backend_import_gallery_py.txt -πŸ“ backend/ - πŸ“„ scrape_nsg_3.py - πŸ“„ update_admin.py - πŸ“„ test_gemini.py - πŸ“„ import_gallery.py - πŸ“„ import_nye_felter.py - πŸ“„ .env - πŸ“„ VtG.txt - πŸ“„ scrape_membership.py - πŸ“„ test_login.py - πŸ“„ sync_greenfee.py - πŸ“„ scrape_greenfee.py - πŸ“„ scrape_status.py - πŸ“„ scrape_golfamore1.3.py - πŸ“„ requirements.txt - πŸ“„ import_wp.py - πŸ“„ create_admin.py - πŸ“„ scrape_vtg.py - πŸ“„ import_urls.py - πŸ“„ GreenFee.txt - πŸ“„ Medlemsskap.txt - πŸ“„ main.py - πŸ“„ Dockerfile - πŸ“ public/ - πŸ“ media/ \ No newline at end of file diff --git a/fil-tre.txt b/fil-tre.txt deleted file mode 100644 index 7d204ab..0000000 --- a/fil-tre.txt +++ /dev/null @@ -1,735 +0,0 @@ - πŸ“ teeoff/ - πŸ“„ seed.sql - πŸ“„ eksport_script.py - πŸ“„ update_golfbox.sql - πŸ“„ docker-compose.yml - πŸ“„ schema.sql - πŸ“„ init.sql -πŸ“ frontend/ - πŸ“„ eslint.config.mjs - πŸ“„ next-env.d.ts - πŸ“„ tsconfig.json - πŸ“„ README.md - πŸ“„ next.config.ts - πŸ“„ postcss.config.mjs - πŸ“„ package-lock.json - πŸ“„ .gitignore - πŸ“„ package.json - πŸ“„ Dockerfile - πŸ“ public/ - πŸ“„ globe.svg - πŸ“„ vercel.svg - πŸ“„ Toppbilde-standard.jpg - πŸ“„ TeeOff-logo-Retina-1.png - πŸ“„ window.svg - πŸ“„ next.svg - πŸ“„ file.svg - πŸ“ media/ - πŸ“„ slide_naeroysund-golfklubb_5.jpg - πŸ“„ main_hakadal-golfklubb.jpg - πŸ“„ slide_baerum-golfklubb_1.jpg - πŸ“„ slide_egersund-golfklubb_7.jpg - πŸ“„ main_hammerfest-og-kvalsund-golfklubb.jpg - πŸ“„ main_odda-golfklubb.jpg - πŸ“„ slide_holtsmark-golfklubb_1.jpg - πŸ“„ main_voss-golfklubb.jpeg - πŸ“„ slide_soon-golfklubb_2.jpg - πŸ“„ logo_ogna-golfklubb.jpg - πŸ“„ slide_tyrifjord-golfklubb_4.jpg - πŸ“„ slide_tjome-golfklubb_2.jpg - πŸ“„ logo_garder-golfklubb.jpg - πŸ“„ slide_helgeland-golfklubb_1.jpg - πŸ“„ slide_kvinnherad-golfklubb_0.jpg - πŸ“„ main_rauma-golfklubb.jpg - πŸ“„ slide_naeroysund-golfklubb_0.jpg - πŸ“„ slide_hallingdal-golfklubb_1.jpeg - πŸ“„ slide_hakadal-golfklubb_13.jpg - πŸ“„ slide_sunndal-golfklubb_2.jpg - πŸ“„ logo_skjeberg-golfklubb.jpg - πŸ“„ slide_larvik-golfklubb_0.jpg - πŸ“„ logo_tingvoll-golfklubb.jpg - πŸ“„ slide_sandefjord-golfklubb_4.jpg - πŸ“„ logo_onsoy-golfklubb.jpg - πŸ“„ logo_oppdal-golfklubb.jpg - πŸ“„ main_nordhaug-golfklubb.jpg - πŸ“„ main_hof-golfklubb.jpg - πŸ“„ main_hauger-golfklubb.jpg - πŸ“„ slide_vradal-golfklubb_1.jpg - πŸ“„ slide_holtsmark-golfklubb_5.jpg - πŸ“„ main_baerum-golfklubb.jpg - πŸ“„ logo_huseby-hanko-golfklubb.png - πŸ“„ slide_solum-golfklubb_4.jpg - πŸ“„ logo_hardanger-golfklubb.png - πŸ“„ logo_kristiansund-og-omegn-golfklubb.jpg - πŸ“„ main_byneset-golf.jpg - πŸ“„ logo_laerdal-golfklubb.jpg - πŸ“„ slide_kjekstad-golfklubb_0.jpg - πŸ“„ main_namdal-golfklubb.jpg - πŸ“„ main_hallingdal-golfklubb.jpg - πŸ“„ logo_ibestad-golfklubb.jpg - πŸ“„ logo_alta-golfklubb.png - πŸ“„ slide_norsjo-golfklubb_4.jpg - πŸ“„ main_elverum-golfklubb.jpg - πŸ“„ logo_hammerfest-og-kvalsund-golfklubb.jpg - πŸ“„ main_garder-golfklubb.jpg - πŸ“„ logo_lommedalen-golfklubb.jpg - πŸ“„ slide_ski-golfklubb_1.jpg - πŸ“„ logo_gronmo-golfklubb.jpg - πŸ“„ logo_hvam-golfklubb.jpg - πŸ“„ main_nesbyen-golfklubb.jpg - πŸ“„ slide_naeroysund-golfklubb_2.jpg - πŸ“„ slide_tjome-golfklubb_0.jpg - πŸ“„ logo_miklagard-golfklubb.png - πŸ“„ main_nordfjord-golfklubb.jpg - πŸ“„ slide_bjaavann-golfklubb_1.jpg - πŸ“„ main_jaeren-golfklubb.jpg - πŸ“„ slide_salten-golfklubb-bodo-golfpark_1.jpg - πŸ“„ slide_larvik-golfklubb_1.jpg - πŸ“„ main_modum-golfklubb.jpg - πŸ“„ logo_volda-golfklubb.jpg - πŸ“„ main_grini-golfklubb.jpg - πŸ“„ slide_aurskog-golfpark_0.jpg - πŸ“„ main_miklagard-golfklubb.jpg - πŸ“„ main_bjaavann-golfklubb.jpg - πŸ“„ main_fet-golfklubb.jpg - πŸ“„ slide_atlungstad-golfklubb_2.jpg - πŸ“„ logo_kvinnherad-golfklubb.jpg - πŸ“„ slide_borregaard-golfklubb_1.jpg - πŸ“„ slide_tjome-golfklubb_4.jpg - πŸ“„ main_rjukan-og-tinn-golfklubb.jpg - πŸ“„ slide_kjekstad-golfklubb_1.jpg - πŸ“„ slide_norsjo-golfklubb_3.jpg - πŸ“„ logo_surnadal-golfklubb.png - πŸ“„ slide_kongsberg-golfklubb_0.jpg - πŸ“„ slide_hovden-golfklubb_1.jpg - πŸ“„ main_hemsedal-golfklubb.jpg - πŸ“„ slide_tyrifjord-golfklubb_5.jpg - πŸ“„ logo_alesund-golfklubb.jpg - πŸ“„ slide_tingvoll-golfklubb_0.jpg - πŸ“„ slide_tyrifjord-golfklubb_2.jpg - πŸ“„ slide_bleik-golfstrombane_0.jpg - πŸ“„ slide_nesbyen-golfklubb_0.jpg - πŸ“„ main_sunnfjord-golfklubb.jpg - πŸ“„ slide_hasvik-golfklubb_0.jpg - πŸ“„ logo_stavanger-golfklubb.png - πŸ“„ slide_ostmarka-golfklubb_3.jpg - πŸ“„ main_land-golfklubb.jpg - πŸ“„ slide_helgeland-golfklubb_5.jpg - πŸ“„ logo_bergen-golfklubb.png - πŸ“„ slide_lofoten-golfklubb_1.jpg - πŸ“„ logo_rjukan-og-tinn-golfklubb.jpg - πŸ“„ main_alta-golfklubb.jpg - πŸ“„ slide_asker-golfklubb_1.jpg - πŸ“„ slide_krokhol-golfklubb_5.jpg - πŸ“„ logo_nordvegen-golfklubb.png - πŸ“„ logo_kristiansand-golfklubb.jpg - πŸ“„ main_asker-golfklubb.jpg - πŸ“„ slide_sleneset-golfklubb_0.jpg - πŸ“„ logo_re-golfklubb.jpg - πŸ“„ slide_oustoen-country-club_0.jpg - πŸ“„ logo_hallingdal-golfklubb.png - πŸ“„ logo_fet-golfklubb.jpg - πŸ“„ slide_trondheim-golfklubb_3.jpg - πŸ“„ slide_borregaard-golfklubb_3.jpg - πŸ“„ main_hovden-golfklubb.jpg - πŸ“„ slide_ostmarka-golfklubb_2.jpg - πŸ“„ slide_sandane-golfklubb_2.jpg - πŸ“„ slide_trondheim-golfklubb_6.jpg - πŸ“„ slide_helgeland-golfklubb_9.jpg - πŸ“„ logo_borre-golfklubb.png - πŸ“„ main_karasjok-golfklubb.jpg - πŸ“„ slide_hallingdal-golfklubb_2.jpeg - πŸ“„ main_north-cape-golf-club.jpg - πŸ“„ logo_selbu-golfklubb.png - πŸ“„ logo_sola-golfklubb-forus.jpg - πŸ“„ slide_asker-golfklubb_0.jpg - πŸ“„ main_hafjell-golfklubb.jpg - πŸ“„ logo_mjosen-golfklubb.png - πŸ“„ slide_atlungstad-golfklubb_0.jpg - πŸ“„ main_moa-golfsenter.jpg - πŸ“„ slide_bleik-golfstrombane_7.jpg - πŸ“„ main_kvinnherad-golfklubb.jpg - πŸ“„ main_re-golfklubb.jpg - πŸ“„ logo_haga-golfklubb.jpg - πŸ“„ main_roros-golfklubb.jpg - πŸ“„ main_vestfold-golfklubb.jpg - πŸ“„ slide_egersund-golfklubb_2.jpg - πŸ“„ slide_volda-golfklubb_2.jpeg - πŸ“„ slide_egersund-golfklubb_5.jpg - πŸ“„ main_gjersjoen-golfklubb.jpg - πŸ“„ main_gamle-fredrikstad-golfklubb.jpg - πŸ“„ slide_naeroysund-golfklubb_3.jpg - πŸ“„ slide_oustoen-country-club_1.jpg - πŸ“„ logo_hitra-golfklubb.png - πŸ“„ logo_rygge-flystasjon-golf-klubb.jpg - πŸ“„ slide_tyrifjord-golfklubb_9.jpg - πŸ“„ slide_trondheim-golfklubb_5.jpg - πŸ“„ slide_aurskog-golfpark_1.jpg - πŸ“„ main_sola-golfklubb-solastranden.jpg - πŸ“„ logo_trysil-golfklubb.jpg - πŸ“„ logo_harstad-golfklubb.jpg - πŸ“„ slide_ogna-golfklubb_1.jpg - πŸ“„ logo_gamle-fredrikstad-golfklubb.jpg - πŸ“„ main_stranda-golfklubb.jpg - πŸ“„ logo_sauda-golfklubb.png - πŸ“„ slide_egersund-golfklubb_0.jpg - πŸ“„ logo_solum-golfklubb.jpg - πŸ“„ main_bjornefjorden-golfklubb.jpg - πŸ“„ main_vestlia-golf.jpg - πŸ“„ slide_sandefjord-golfklubb_1.jpg - πŸ“„ logo_borregaard-golfklubb.jpg - πŸ“„ main_grimstad-golfklubb.jpg - πŸ“„ logo_vesteralen-golfklubb.png - πŸ“„ main_kristiansund-og-omegn-golfklubb.jpg - πŸ“„ slide_hallingdal-golfklubb_3.jpeg - πŸ“„ logo_sandnes-golfklubb.jpg - πŸ“„ main_alesund-golfklubb.jpeg - πŸ“„ main_steinkjer-golfklubb.jpg - πŸ“„ main_skei-golfklubb.jpg - πŸ“„ slide_nesbyen-golfklubb_3.jpg - πŸ“„ slide_hardanger-golfklubb_0.jpg - πŸ“„ slide_gumoy-golf_1.jpg - πŸ“„ main_grenland-og-omegn-golfklubb.jpg - πŸ“„ main_karmoy-golfklubb.jpg - πŸ“„ logo_egersund-golfklubb.jpg - πŸ“„ logo_baerum-golfklubb.png - πŸ“„ main_kristiansand-golfklubb.jpg - πŸ“„ slide_kongsberg-golfklubb_2.jpg - πŸ“„ slide_gumoy-golf_3.jpg - πŸ“„ logo_smola-golfklubb.jpg - πŸ“„ logo_grimstad-golfklubb.png - πŸ“„ logo_stord-golfklubb.png - πŸ“„ slide_helgeland-golfklubb_6.jpg - πŸ“„ logo_rauma-golfklubb.png - πŸ“„ logo_hinnoy-golfklubb.jpg - πŸ“„ slide_holtsmark-golfklubb_0.jpg - πŸ“„ logo_varanger-golfklubb.jpg - πŸ“„ logo_mandal-golfklubb.jpg - πŸ“„ slide_trondheim-golfklubb_1.jpg - πŸ“„ logo_tjome-golfklubb.png - πŸ“„ slide_lofoten-golfklubb_2.jpg - πŸ“„ main_haga-golfklubb.jpg - πŸ“„ slide_bjaavann-golfklubb_2.jpg - πŸ“„ slide_hakadal-golfklubb_8.jpg - πŸ“„ logo_herdla-golfklubb.jpg - πŸ“„ main_stavanger-golfklubb.jpg - πŸ“„ main_lommedalen-golfklubb.jpg - πŸ“„ main_sirdal-fjellgolf-klubb.jpg - πŸ“„ main_alsten-golfklubb.jpg - πŸ“„ logo_lillestrom-golfklubb.png - πŸ“„ main_ogna-golfklubb.jpg - πŸ“„ slide_helgeland-golfklubb_8.jpg - πŸ“„ slide_nesbyen-golfklubb_2.jpg - πŸ“„ main_tjome-golfklubb.jpg - πŸ“„ main_sandefjord-golfklubb.jpg - πŸ“„ main_namsos-golfklubb.jpg - πŸ“„ logo_notteroy-golfklubb.jpg - πŸ“„ main_trondheim-golfklubb.jpg - πŸ“„ slide_solum-golfklubb_1.jpg - πŸ“„ slide_kongsberg-golfklubb_4.jpg - πŸ“„ main_utsikten-golfklubb.jpg - πŸ“„ main_vesteralen-golfklubb.jpg - πŸ“„ slide_sandefjord-golfklubb_0.jpg - πŸ“„ slide_hakadal-golfklubb_9.jpg - πŸ“„ logo_polarsirkelen-golfklubb.jpg - πŸ“„ logo_sandane-golfklubb.jpg - πŸ“„ slide_groruddalen-golfklubb_2.jpg - πŸ“„ main_ski-golfklubb.jpg - πŸ“„ logo_bjornefjorden-golfklubb.png - πŸ“„ main_polarsirkelen-golfklubb.jpg - πŸ“„ slide_krokhol-golfklubb_1.jpg - πŸ“„ main_groruddalen-golfklubb.jpg - πŸ“„ slide_krokhol-golfklubb_4.jpg - πŸ“„ main_vanylven-golfklubb.jpg - πŸ“„ main_borregaard-golfklubb.jpg - πŸ“„ slide_sandefjord-golfklubb_5.jpg - πŸ“„ slide_helgeland-golfklubb_2.jpg - πŸ“„ main_hinnoy-golfklubb.jpg - πŸ“„ slide_sunndal-golfklubb_0.jpg - πŸ“„ logo_groruddalen-golfklubb.png - πŸ“„ main_tingvoll-golfklubb.jpg - πŸ“„ slide_tjome-golfklubb_3.jpg - πŸ“„ main_lindesnes-golfklubb.jpg - πŸ“„ slide_kongsvingers-golfklubb_5.jpg - πŸ“„ slide_tromso-golfklubb_0.jpg - πŸ“„ main_randaberg-golfklubb.jpg - πŸ“„ logo_krokhol-golfklubb.png - πŸ“„ main_smola-golfklubb.jpg - πŸ“„ logo_sande-golfklubb.jpg - πŸ“„ logo_oslo-golfklubb.jpg - πŸ“„ logo_preikestolen.jpg - πŸ“„ logo_sunnfjord-golfklubb.jpg - πŸ“„ slide_lofoten-golfklubb_3.jpg - πŸ“„ main_bleik-golfstrombane.jpg - πŸ“„ slide_gumoy-golf_0.jpg - πŸ“„ logo_modum-golfklubb.png - πŸ“„ slide_tingvoll-golfklubb_1.jpg - πŸ“„ logo_bodo-golfklubb.jpg - πŸ“„ logo_nesbyen-golfklubb.png - πŸ“„ main_selbu-golfklubb.jpg - πŸ“„ slide_holtsmark-golfklubb_2.jpg - πŸ“„ logo_imjelt-pitch-putt.jpg - πŸ“„ logo_ullensaker-golfklubb.png - πŸ“„ slide_elverum-golfklubb_1.jpg - πŸ“„ logo_ringerike-golfklubb.jpg - πŸ“„ slide_hakadal-golfklubb_6.jpg - πŸ“„ slide_volda-golfklubb_4.jpg - πŸ“„ slide_tyrifjord-golfklubb_6.jpg - πŸ“„ main_solum-golfklubb.jpg - πŸ“„ slide_helgeland-golfklubb_4.jpg - πŸ“„ main_stiklestad-golfklubb.jpg - πŸ“„ slide_stavanger-golfklubb_0.jpg - πŸ“„ logo_vestlia-golf.jpg - πŸ“„ slide_vestfold-golfklubb_0.jpg - πŸ“„ logo_floro-golfklubb.jpg - πŸ“„ logo_voss-golfklubb.jpg - πŸ“„ slide_tyrifjord-golfklubb_8.jpg - πŸ“„ slide_sotra-golfklubb_2.jpg - πŸ“„ logo_jaeren-golfklubb.png - πŸ“„ slide_moss-rygge-golfklubb_0.jpg - πŸ“„ logo_asker-golfklubb.png - πŸ“„ slide_bleik-golfstrombane_6.jpg - πŸ“„ slide_bjaavann-golfklubb_3.jpg - πŸ“„ main_hvam-golfklubb.jpg - πŸ“„ logo_eiker-golfklubb.jpg - πŸ“„ slide_bleik-golfstrombane_9.jpg - πŸ“„ slide_namdal-golfklubb_0.jpg - πŸ“„ logo_nordhaug-golfklubb.jpg - πŸ“„ slide_borregaard-golfklubb_0.jpg - πŸ“„ slide_kongsberg-golfklubb_5.jpg - πŸ“„ main_hardanger-golfklubb.jpg - πŸ“„ slide_ostmarka-golfklubb_5.jpg - πŸ“„ logo_oppegard-golfklubb.png - πŸ“„ slide_vradal-golfklubb_0.jpg - πŸ“„ main_narvik-golfklubb.jpg - πŸ“„ logo_midt-troms-golfklubb.jpg - πŸ“„ slide_solum-golfklubb_2.jpg - πŸ“„ slide_sunndal-golfklubb_1.jpg - πŸ“„ slide_gjersjoen-golfklubb_1.jpg - πŸ“„ slide_preikestolen_0.jpg - πŸ“„ main_huseby-hanko-golfklubb.jpg - πŸ“„ slide_holtsmark-golfklubb_4.jpg - πŸ“„ main_larvik-golfklubb.jpg - πŸ“„ slide_hasvik-golfklubb_1.jpg - πŸ“„ slide_herdla-golfklubb_1.jpg - πŸ“„ logo_kjekstad-golfklubb.jpg - πŸ“„ main_austratt-golfklubb.jpg - πŸ“„ slide_sandefjord-golfklubb_3.jpg - πŸ“„ logo_hakadal-golfklubb.png - πŸ“„ slide_bleik-golfstrombane_3.jpg - πŸ“„ logo_vanylven-golfklubb.jpg - πŸ“„ main_skjeberg-golfklubb.jpg - πŸ“„ slide_hovden-golfklubb_3.jpg - πŸ“„ slide_ostmarka-golfklubb_0.jpg - πŸ“„ logo_tromso-golfklubb.png - πŸ“„ main_stjordal-golfklubb.jpg - πŸ“„ slide_frosta-golfklubb_3.jpg - πŸ“„ main_nordvegen-golfklubb.jpg - πŸ“„ slide_norsjo-golfklubb_1.jpg - πŸ“„ logo_gumoy-golf.png - πŸ“„ slide_gronmo-golfklubb_2.jpg - πŸ“„ slide_hardanger-golfklubb_1.jpg - πŸ“„ main_oppdal-golfklubb.jpg - πŸ“„ slide_trondheim-golfklubb_0.jpg - πŸ“„ main_lillestrom-golfklubb.jpg - πŸ“„ slide_gjersjoen-golfklubb_0.jpg - πŸ“„ slide_soon-golfklubb_1.jpg - πŸ“„ logo_vestfold-golfklubb.jpg - πŸ“„ slide_stavanger-golfklubb_1.jpg - πŸ“„ main_norefjell-golfklubb.jpg - πŸ“„ main_kongsberg-golfklubb.jpg - πŸ“„ slide_gjovik-og-toten-golfklubb_2.jpg - πŸ“„ logo_veierland-golfklubb.jpg - πŸ“„ slide_soon-golfklubb_0.jpg - πŸ“„ slide_egersund-golfklubb_6.jpg - πŸ“„ slide_norsjo-golfklubb_0.jpg - πŸ“„ slide_kristiansand-golfklubb_0.jpg - πŸ“„ main_hurum-golfklubb.jpg - πŸ“„ main_sola-golfklubb-forus.jpg - πŸ“„ logo_mork-golfklubb.jpg - πŸ“„ logo_molde-golfklubb.png - πŸ“„ main_kongsvingers-golfklubb.jpg - πŸ“„ main_sunndal-golfklubb.jpg - πŸ“„ logo_utsikten-golfklubb.jpg - πŸ“„ logo_austratt-golfklubb.jpg - πŸ“„ logo_selje-golfklubb.jpg - πŸ“„ main_stord-golfklubb.jpg - πŸ“„ logo_sorknes-golfklubb.jpg - πŸ“„ slide_krokhol-golfklubb_3.jpg - πŸ“„ slide_hovden-golfklubb_4.jpg - πŸ“„ main_preikestolen.jpg - πŸ“„ logo_aurskog-golfpark.png - πŸ“„ main_aurskog-golfpark.jpg - πŸ“„ slide_mork-golfklubb_1.jpg - πŸ“„ logo_roros-golfklubb.jpg - πŸ“„ main_harstad-golfklubb.jpg - πŸ“„ main_helgeland-golfklubb.jpg - πŸ“„ slide_ogna-golfklubb_0.jpg - πŸ“„ slide_kvinnherad-golfklubb_1.jpg - πŸ“„ logo_haugesund-golfklubb.png - πŸ“„ slide_bjaavann-golfklubb_0.jpg - πŸ“„ slide_hakadal-golfklubb_3.jpg - πŸ“„ slide_ostmarka-golfklubb_6.jpg - πŸ“„ slide_bleik-golfstrombane_11.jpg - πŸ“„ logo_moss-rygge-golfklubb.png - πŸ“„ slide_borregaard-golfklubb_2.jpg - πŸ“„ main_lofoten-golfklubb.jpg - πŸ“„ slide_bleik-golfstrombane_5.jpg - πŸ“„ logo_oustoen-country-club.jpg - πŸ“„ main_oslo-golfklubb.jpg - πŸ“„ slide_gjovik-og-toten-golfklubb_0.jpg - πŸ“„ slide_frosta-golfklubb_1.jpg - πŸ“„ slide_ostmarka-golfklubb_7.jpg - πŸ“„ slide_helgeland-golfklubb_3.jpg - πŸ“„ logo_vradal-golfklubb.png - πŸ“„ main_salten-golfklubb-bodo-golfpark.jpg - πŸ“„ main_rygge-flystasjon-golf-klubb.jpg - πŸ“„ slide_trondheim-golfklubb_4.jpg - πŸ“„ main_meland-golfklubb.jpg - πŸ“„ slide_sotra-golfklubb_1.jpg - πŸ“„ main_sandane-golfklubb.jpg - πŸ“„ slide_baerum-golfklubb_0.jpg - πŸ“„ slide_egersund-golfklubb_3.jpg - πŸ“„ slide_frosta-golfklubb_5.jpg - πŸ“„ logo_sandefjord-golfklubb.png - πŸ“„ slide_egersund-golfklubb_4.jpg - πŸ“„ slide_sandane-golfklubb_1.jpg - πŸ“„ slide_bleik-golfstrombane_2.jpg - πŸ“„ main_eiker-golfklubb.jpg - πŸ“„ main_tromso-golfklubb.jpg - πŸ“„ logo_tysnes-golfklubb.jpg - πŸ“„ slide_hallingdal-golfklubb_4.jpeg - πŸ“„ main_ringerike-golfklubb.jpg - πŸ“„ main_norsjo-golfklubb.jpg - πŸ“„ logo_byneset-golf.jpg - πŸ“„ logo_land-golfklubb.png - πŸ“„ slide_bleik-golfstrombane_8.jpg - πŸ“„ logo_losby-golfklubb.png - πŸ“„ logo_husoy-golfklubb.png - πŸ“„ main_nittedal-golfklubb.jpg - πŸ“„ logo_trondheim-par3golf-havstein.png - πŸ“„ main_mork-golfklubb.jpg - πŸ“„ slide_sleneset-golfklubb_1.jpg - πŸ“„ logo_ekholtbruket-golfklubb.jpg - πŸ“„ slide_hauger-golfklubb_1.jpg - πŸ“„ main_borre-golfklubb.jpg - πŸ“„ main_kragero-golfklubb.jpg - πŸ“„ main_lonne-golfklubb.jpg - πŸ“„ logo_namsos-golfklubb.jpg - πŸ“„ slide_hallingdal-golfklubb_0.jpeg - πŸ“„ slide_solum-golfklubb_0.jpg - πŸ“„ main_holtsmark-golfklubb.jpg - πŸ“„ main_oustoen-country-club.jpg - πŸ“„ slide_hakadal-golfklubb_12.jpg - πŸ“„ slide_gronmo-golfklubb_1.jpg - πŸ“„ main_mjosen-golfklubb.jpg - πŸ“„ logo_sunndal-golfklubb.jpg - πŸ“„ logo_larvik-golfklubb.jpg - πŸ“„ slide_ekholtbruket-golfklubb_0.jpg - πŸ“„ main_husoy-golfklubb.jpg - πŸ“„ logo_drammen-golfklubb.png - πŸ“„ logo_helgeland-golfklubb.png - πŸ“„ main_gjerdrum-golfklubb.jpg - πŸ“„ slide_sandefjord-golfklubb_2.jpg - πŸ“„ main_hvaler-golfklubb.jpg - πŸ“„ logo_sunnmore-golfklubb.jpg - πŸ“„ slide_hauger-golfklubb_0.jpg - πŸ“„ main_naeroysund-golfklubb.jpg - πŸ“„ slide_hakadal-golfklubb_5.jpg - πŸ“„ logo_bamble-golfklubb.png - πŸ“„ slide_sandane-golfklubb_0.jpg - πŸ“„ main_bodo-golfklubb.jpeg - πŸ“„ main_drammen-golfklubb.jpg - πŸ“„ slide_frosta-golfklubb_4.jpg - πŸ“„ slide_solum-golfklubb_3.jpg - πŸ“„ slide_bleik-golfstrombane_1.jpg - πŸ“„ main_trondheim-par3golf-havstein.jpg - πŸ“„ logo_kongsvingers-golfklubb.jpg - πŸ“„ slide_volda-golfklubb_3.jpeg - πŸ“„ slide_tyrifjord-golfklubb_3.jpg - πŸ“„ slide_hakadal-golfklubb_2.jpg - πŸ“„ slide_hovden-golfklubb_2.jpg - πŸ“„ slide_kongsvingers-golfklubb_4.jpg - πŸ“„ main_arendal-omegn-golfklubb.jpg - πŸ“„ logo_bjaavann-golfklubb.png - πŸ“„ main_sande-golfklubb.jpg - πŸ“„ main_ballerud-golfklubb.jpg - πŸ“„ logo_norefjell-golfklubb.png - πŸ“„ slide_kristiansand-golfklubb_1.jpg - πŸ“„ main_veierland-golfklubb.jpg - πŸ“„ logo_karmoy-golfklubb.png - πŸ“„ logo_lofoten-golfklubb.png - πŸ“„ slide_lofoten-golfklubb_0.jpg - πŸ“„ logo_hurum-golfklubb.png - πŸ“„ slide_holtsmark-golfklubb_3.jpg - πŸ“„ logo_hemsedal-golfklubb.jpg - πŸ“„ slide_borregaard-golfklubb_5.jpg - πŸ“„ logo_haugaland-golfklubb.png - πŸ“„ slide_bjornefjorden-golfklubb_1.jpg - πŸ“„ main_notteroy-golfklubb.jpg - πŸ“„ slide_kongsvingers-golfklubb_3.jpg - πŸ“„ slide_kongsvingers-golfklubb_2.jpg - πŸ“„ main_fana-golfklubb.jpg - πŸ“„ logo_giske-golfklubb.jpg - πŸ“„ slide_hakadal-golfklubb_11.jpg - πŸ“„ main_nes-golfklubb-09.jpg - πŸ“„ main_volda-golfklubb.jpeg - πŸ“„ logo_arendal-omegn-golfklubb.png - πŸ“„ slide_gjovik-og-toten-golfklubb_1.jpg - πŸ“„ main_soon-golfklubb.jpg - πŸ“„ logo_soon-golfklubb.png - πŸ“„ slide_gumoy-golf_2.jpg - πŸ“„ logo_alsten-golfklubb.png - πŸ“„ main_halden-golfklubb.jpg - πŸ“„ logo_narvik-golfklubb.jpg - πŸ“„ logo_randaberg-golfklubb.jpg - πŸ“„ slide_hakadal-golfklubb_10.jpg - πŸ“„ main_ostmarka-golfklubb.jpg - πŸ“„ logo_bleik-golfstrombane.jpg - πŸ“„ logo_hasvik-golfklubb.png - πŸ“„ main_floro-golfklubb.jpg - πŸ“„ main_gronmo-golfklubb.jpg - πŸ“„ slide_hakadal-golfklubb_7.jpg - πŸ“„ slide_egersund-golfklubb_8.jpg - πŸ“„ logo_atlungstad-golfklubb.png - πŸ“„ main_sunnmore-golfklubb.jpeg - πŸ“„ main_sleneset-golfklubb.jpg - πŸ“„ logo_lonne-golfklubb.png - πŸ“„ main_haugaland-golfklubb.jpg - πŸ“„ main_egersund-golfklubb.jpg - πŸ“„ slide_solum-golfklubb_5.jpg - πŸ“„ slide_gamle-fredrikstad-golfklubb_1.jpg - πŸ“„ main_sotra-golfklubb.jpg - πŸ“„ main_trysil-golfklubb.jpg - πŸ“„ logo_kongsberg-golfklubb.png - πŸ“„ slide_tromso-golfklubb_1.jpg - πŸ“„ logo_frosta-golfklubb.jpg - πŸ“„ slide_groruddalen-golfklubb_1.jpg - πŸ“„ logo_hafjell-golfklubb.png - πŸ“„ logo_sola-golfklubb-solastranden.png - πŸ“„ logo_holtsmark-golfklubb.png - πŸ“„ slide_naeroysund-golfklubb_1.jpg - πŸ“„ slide_groruddalen-golfklubb_0.jpg - πŸ“„ slide_naeroysund-golfklubb_4.jpg - πŸ“„ slide_kjekstad-golfklubb_2.jpg - πŸ“„ slide_gronmo-golfklubb_0.jpg - πŸ“„ slide_preikestolen_1.jpg - πŸ“„ logo_fana-golfklubb.jpg - πŸ“„ slide_sorknes-golfklubb_0.jpg - πŸ“„ slide_atlungstad-golfklubb_3.jpg - πŸ“„ main_losby-golfklubb.jpg - πŸ“„ slide_borregaard-golfklubb_4.jpg - πŸ“„ slide_hakadal-golfklubb_1.jpg - πŸ“„ main_sandnes-golfklubb.jpg - πŸ“„ slide_norsjo-golfklubb_2.jpg - πŸ“„ main_frosta-golfklubb.jpg - πŸ“„ slide_krokhol-golfklubb_0.jpg - πŸ“„ logo_askim-golfklubb.png - πŸ“„ main_stryn-golfklubb.jpg - πŸ“„ slide_lofoten-golfklubb_5.jpg - πŸ“„ logo_karasjok-golfklubb.jpg - πŸ“„ slide_krokhol-golfklubb_2.jpg - πŸ“„ slide_hakadal-golfklubb_14.jpg - πŸ“„ logo_gjovik-og-toten-golfklubb.png - πŸ“„ main_haugesund-golfklubb.jpg - πŸ“„ logo_stjordal-golfklubb.jpg - πŸ“„ logo_drobak-golfklubb.png - πŸ“„ main_atlungstad-golfklubb.jpg - πŸ“„ logo_trondheim-golfklubb.jpg - πŸ“„ logo_gjersjoen-golfklubb.png - πŸ“„ slide_lofoten-golfklubb_4.jpg - πŸ“„ slide_helgeland-golfklubb_7.jpg - πŸ“„ main_molde-golfklubb.jpg - πŸ“„ slide_tyrifjord-golfklubb_7.jpg - πŸ“„ logo_gjerdrum-golfklubb.png - πŸ“„ main_onsoy-golfklubb.jpg - πŸ“„ slide_trondheim-golfklubb_2.jpg - πŸ“„ slide_mork-golfklubb_0.jpg - πŸ“„ slide_frosta-golfklubb_2.jpg - πŸ“„ slide_giske-golfklubb_1.jpeg - πŸ“„ slide_moss-rygge-golfklubb_1.jpg - πŸ“„ slide_ostmarka-golfklubb_1.jpg - πŸ“„ slide_volda-golfklubb_0.jpeg - πŸ“„ logo_sotra-golfklubb.jpg - πŸ“„ main_mandal-golfklubb.jpg - πŸ“„ logo_grenland-og-omegn-golfklubb.jpg - πŸ“„ slide_volda-golfklubb_1.jpeg - πŸ“„ main_imjelt-pitch-putt.jpg - πŸ“„ logo_nittedal-golfklubb.png - πŸ“„ main_gumoy-golf.jpg - πŸ“„ logo_hauger-golfklubb.png - πŸ“„ slide_hakadal-golfklubb_4.jpg - πŸ“„ slide_ostmarka-golfklubb_4.jpg - πŸ“„ slide_hakadal-golfklubb_0.jpg - πŸ“„ main_surnadal-golfklubb.jpg - πŸ“„ logo_hof-golfklubb.png - πŸ“„ slide_sleneset-golfklubb_2.jpg - πŸ“„ logo_halden-golfklubb.png - πŸ“„ logo_naeroysund-golfklubb.png - πŸ“„ logo_elverum-golfklubb.png - πŸ“„ main_bamble-golfklubb.jpg - πŸ“„ logo_kragero-golfklubb.png - πŸ“„ logo_valdres-golfklubb.png - πŸ“„ slide_tyrifjord-golfklubb_1.jpg - πŸ“„ logo_grini-golfklubb.png - πŸ“„ logo_lindesnes-golfklubb.jpg - πŸ“„ slide_lofoten-golfklubb_6.jpg - πŸ“„ main_sauda-golfklubb.jpg - πŸ“„ main_moss-rygge-golfklubb.jpg - πŸ“„ main_dalane-golfklubb.jpg - πŸ“„ logo_skei-golfklubb.jpg - πŸ“„ logo_ostmarka-golfklubb.png - πŸ“„ slide_nesbyen-golfklubb_1.jpg - πŸ“„ logo_stryn-golfklubb.png - πŸ“„ main_vradal-golfklubb.jpg - πŸ“„ slide_hovden-golfklubb_0.jpg - πŸ“„ main_gjovik-og-toten-golfklubb.jpg - πŸ“„ logo_hvaler-golfklubb.png - πŸ“„ main_giske-golfklubb.jpeg - πŸ“„ slide_herdla-golfklubb_0.jpg - πŸ“„ main_ibestad-golfklubb.jpg - πŸ“„ slide_kongsvingers-golfklubb_0.jpg - πŸ“„ slide_atlungstad-golfklubb_1.jpg - πŸ“„ logo_nordfjord-golfklubb.png - πŸ“„ logo_eidskog-golfklubb.png - πŸ“„ main_hitra-golfklubb.jpg - πŸ“„ logo_dalane-golfklubb.jpg - πŸ“„ main_kjekstad-golfklubb.jpg - πŸ“„ logo_odda-golfklubb.jpg - πŸ“„ logo_ballerud-golfklubb.jpg - πŸ“„ main_klaebu-golfklubb.jpg - πŸ“„ main_hasvik-golfklubb.jpg - πŸ“„ logo_klaebu-golfklubb.png - πŸ“„ slide_bleik-golfstrombane_4.jpg - πŸ“„ logo_north-cape-golf-club.jpg - πŸ“„ slide_tyrifjord-golfklubb_0.jpg - πŸ“„ logo_moa-golfsenter.png - πŸ“„ main_herdla-golfklubb.jpg - πŸ“„ slide_gjersjoen-golfklubb_2.jpg - πŸ“„ logo_stiklestad-golfklubb.png - πŸ“„ slide_bleik-golfstrombane_10.jpg - πŸ“„ logo_hovden-golfklubb.jpg - πŸ“„ slide_frosta-golfklubb_0.jpg - πŸ“„ logo_sirdal-fjellgolf-klubb.png - πŸ“„ main_ekholtbruket-golfklubb.jpg - πŸ“„ slide_salten-golfklubb-bodo-golfpark_0.jpg - πŸ“„ main_askim-golfklubb.jpg - πŸ“„ slide_elverum-golfklubb_0.jpg - πŸ“„ slide_gamle-fredrikstad-golfklubb_0.jpg - πŸ“„ main_oppegard-golfklubb.jpg - πŸ“„ slide_sorknes-golfklubb_1.jpg - πŸ“„ main_drobak-golfklubb.jpg - πŸ“„ slide_kongsvingers-golfklubb_1.jpg - πŸ“„ slide_helgeland-golfklubb_0.jpg - πŸ“„ slide_vestfold-golfklubb_1.jpg - πŸ“„ slide_kongsberg-golfklubb_1.jpg - πŸ“„ slide_tjome-golfklubb_1.jpg - πŸ“„ slide_ekholtbruket-golfklubb_1.jpg - πŸ“„ main_bergen-golfklubb.jpg - πŸ“„ logo_salten-golfklubb-bodo-golfpark.jpg - πŸ“„ slide_egersund-golfklubb_1.jpg - πŸ“„ slide_trondheim-golfklubb_7.jpg - πŸ“„ logo_stranda-golfklubb.jpg - πŸ“„ main_eidskog-golfklubb.jpg - πŸ“„ logo_nes-golfklubb-09.jpg - πŸ“„ logo_ski-golfklubb.png - πŸ“„ logo_norsjo-golfklubb.png - πŸ“„ slide_kongsberg-golfklubb_3.jpg - πŸ“„ slide_ski-golfklubb_0.jpg - πŸ“„ main_sorknes-golfklubb.jpg - πŸ“„ main_laerdal-golfklubb.jpg - πŸ“„ slide_giske-golfklubb_0.jpeg - πŸ“„ main_ullensaker-golfklubb.jpg - πŸ“„ main_midt-troms-golfklubb.jpg - πŸ“„ main_tysnes-golfklubb.jpg - πŸ“„ logo_namdal-golfklubb.jpg - πŸ“„ logo_tyrifjord-golfklubb.png - πŸ“„ slide_bjornefjorden-golfklubb_0.jpg - πŸ“„ slide_hovden-golfklubb_5.jpg - πŸ“„ main_tyrifjord-golfklubb.jpg - πŸ“„ slide_sotra-golfklubb_0.jpg - πŸ“„ main_valdres-golfklubb.jpg - πŸ“„ logo_steinkjer-golfklubb.jpg - πŸ“„ main_krokhol-golfklubb.jpg - πŸ“„ main_varanger-golfklubb.jpg - πŸ“„ logo_meland-golfklubb.png - πŸ“„ slide_namdal-golfklubb_1.jpg - πŸ“„ main_selje-golfklubb.jpg - πŸ“ src/ - πŸ“„ struktur_dump.txt - πŸ“„ middleware.ts - πŸ“ components/ - πŸ“„ ScrapeMethodSelect.tsx - πŸ“„ Header.tsx - πŸ“ config/ - πŸ“„ constants.ts - πŸ“ app/ - πŸ“„ FacilitySearch.tsx - πŸ“„ HeroSlider.tsx - πŸ“„ favicon.ico - πŸ“„ globals.css - πŸ“„ page.tsx - πŸ“„ layout.tsx - πŸ“ golfbaner/ - πŸ“ [slug]/ - πŸ“„ CourseDisplay.tsx - πŸ“„ page.tsx - πŸ“„ FacilityDetailView.tsx - πŸ“ admin/ - πŸ“„ page.tsx - πŸ“ rediger/ - πŸ“ [slug]/ - πŸ“„ EditFacilityClient.tsx - πŸ“„ page.tsx - πŸ“ login/ - πŸ“„ page.tsx - πŸ“ vtg/ - πŸ“„ page.tsx - πŸ“ medlemskap/ - πŸ“„ page.tsx - πŸ“ greenfee/ - πŸ“„ page.tsx -πŸ“ kode_eksport_1/ - πŸ“„ frontend_src_components_Header_tsx.txt - πŸ“„ frontend_src_app_admin_medlemskap_page_tsx.txt - πŸ“„ frontend_next-env_d_ts.txt - πŸ“„ frontend_src_app_layout_tsx.txt - πŸ“„ frontend_src_app_admin_vtg_page_tsx.txt - πŸ“„ frontend_src_app_admin_greenfee_page_tsx.txt - πŸ“„ frontend_src_app_page_tsx.txt - πŸ“„ eksport_script_py.txt - πŸ“„ frontend_src_components_ScrapeMethodSelect_tsx.txt - πŸ“„ frontend_src_app_golfbaner_[slug]_page_tsx.txt - πŸ“„ frontend_src_middleware_ts.txt - πŸ“„ frontend_src_app_golfbaner_[slug]_CourseDisplay_tsx.txt - πŸ“„ frontend_next_config_ts.txt - πŸ“„ frontend_src_app_admin_login_page_tsx.txt - πŸ“„ frontend_src_app_golfbaner_[slug]_FacilityDetailView_tsx.txt - πŸ“„ frontend_src_app_admin_rediger_[slug]_page_tsx.txt - πŸ“„ frontend_src_app_admin_page_tsx.txt - πŸ“„ frontend_src_app_admin_rediger_[slug]_EditFacilityClient_tsx.txt - πŸ“„ frontend_src_app_HeroSlider_tsx.txt - πŸ“„ frontend_src_app_FacilitySearch_tsx.txt - πŸ“„ frontend_src_config_constants_ts.txt -πŸ“ backend/ - πŸ“„ scrape_nsg_3.py - πŸ“„ update_admin.py - πŸ“„ test_gemini.py - πŸ“„ import_gallery.py - πŸ“„ import_nye_felter.py - πŸ“„ .env - πŸ“„ scrape_membership.py - πŸ“„ test_login.py - πŸ“„ sync_greenfee.py - πŸ“„ scrape_greenfee.py - πŸ“„ scrape_status.py - πŸ“„ scrape_golfamore1.3.py - πŸ“„ requirements.txt - πŸ“„ import_wp.py - πŸ“„ create_admin.py - πŸ“„ scrape_vtg.py - πŸ“„ import_urls.py - πŸ“„ main.py - πŸ“„ Dockerfile - πŸ“ public/ - πŸ“ media/ \ No newline at end of file diff --git a/frontend/src/struktur_dump.txt b/frontend/src/struktur_dump.txt deleted file mode 100644 index e69de29..0000000 diff --git a/kode_eksport_1/backend_create_admin_py.txt b/kode_eksport_1/backend_create_admin_py.txt deleted file mode 100644 index c2d3dcb..0000000 --- a/kode_eksport_1/backend_create_admin_py.txt +++ /dev/null @@ -1,64 +0,0 @@ -""" -TEE OFF ADMIN GENERATOR v1.9 (DEBUG & BULLETPROOF) ---------------------------------------------------------------------------- -FUNKSJON: Genererer SQL-kommando for administrator. -STATUS: Beholder TRUNCATE for feilsΓΈking, men sikrer SQL-innsendingen. ---------------------------------------------------------------------------- -""" -import pyotp -from passlib.hash import pbkdf2_sha256 -import getpass -import sys - -def generate_admin(): - print("\n" + "="*50) - print(" TEE OFF ADMIN GENERATOR v1.9 (DEBUG MODE)") - print("="*50) - - username = input("Brukernavn (f.eks Envide Webutvikling): ").strip() - email = input("E-post: ").strip() - - # Sikre mot SQL-feil hvis navnet/eposten inneholder apostrof - safe_username = username.replace("'", "''") - safe_email = email.replace("'", "''") - - # Passord-verifisering - while True: - password = getpass.getpass("Skriv inn passord: ") - password_confirm = getpass.getpass("Gjenta passord: ") - - if password == password_confirm: - if len(password) < 8: - print("⚠️ Advarsel: Passordet bΓΈr vΓ¦re minst 8 tegn.") - print(f"\n[DEBUG] Passord akseptert. Lengde: {len(password)} tegn.") - break - else: - print("❌ Passordene er ikke like. PrΓΈv igjen.\n") - - otp_secret = pyotp.random_base32() - - print("⏳ Genererer PBKDF2-hash...") - password_hash = pbkdf2_sha256.hash(password) - print(f"[DEBUG] Hash generert. Lengde: {len(password_hash)} tegn.") - - print("\nβœ… GENERERING VELLYKKET!") - print("-" * 50) - print("SLIK LEGGER DU INN BRUKEREN TRYGT:") - print("-" * 50) - print("1. GΓ₯ inn i databasen:") - print(" docker exec -it teeoff_db psql -U teeoff_admin -d teeoff") - print("\n2. Lim inn disse to linjene nΓΈyaktig slik de stΓ₯r:") - print("TRUNCATE admins;") - print(f"INSERT INTO admins (username, email, password_hash, otp_secret) VALUES ('{safe_username}', '{safe_email}', '{password_hash}', '{otp_secret}');") - print("\n3. Skriv 'exit' for Γ₯ gΓ₯ ut.") - print("-" * 50) - print("4. KONFIGURER 2FA I GOOGLE AUTHENTICATOR:") - print(f"Bruk denne nΓΈkkelen: {otp_secret}") - print("-" * 50 + "\n") - -if __name__ == "__main__": - try: - generate_admin() - except KeyboardInterrupt: - print("\nAvbrutt.") - sys.exit(0) \ No newline at end of file diff --git a/kode_eksport_1/backend_import_gallery_py.txt b/kode_eksport_1/backend_import_gallery_py.txt deleted file mode 100644 index 116aa85..0000000 --- a/kode_eksport_1/backend_import_gallery_py.txt +++ /dev/null @@ -1,111 +0,0 @@ -import asyncio -import asyncpg -import urllib.request -import json - -DB_URL = "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff" - -async def fetch_json(url): - """Hjelpefunksjon for Γ₯ hente JSON fra en URL""" - try: - req = urllib.request.Request(url, headers={'User-Agent': 'TeeOff-Migrator/2.0'}) - with urllib.request.urlopen(req) as response: - return json.loads(response.read().decode()) - except Exception as e: - # print(f"⚠️ Kunne ikke hente {url}: {e}") - return None - -async def fetch_media_urls_by_ids(media_ids): - """Henter URLer for en liste med media-IDer (ACF Slides)""" - if not media_ids or not isinstance(media_ids, list) or len(media_ids) == 0: - return [] - - valid_ids = [str(mid) for mid in media_ids if isinstance(mid, (int, str)) and str(mid).isdigit()] - if not valid_ids: return [] - - ids_str = ",".join(valid_ids) - url = f"https://teeoff.no/wp-json/wp/v2/media?include={ids_str}" - data = await fetch_json(url) - - urls = [] - if data: - for m in data: - if 'source_url' in m: - urls.append(m['source_url']) - return urls - -async def run_robust_import(): - print("πŸ•΅οΈβ€β™‚οΈ Starter den store bildejakten (sjekker bΓ₯de Utvalgt bilde og Slides)...") - conn = await asyncpg.connect(DB_URL) - - # VIKTIG: Vi tΓΈmmer tabellen for Γ₯ starte med blanke ark og unngΓ₯ duplikater - await conn.execute("TRUNCATE facility_images CASCADE;") - print("πŸ—‘οΈ TΓΈmte gammel bilde-tabell. Starter import...") - - # Hent alle anleggene fra vΓ₯r egen database - facilities = await conn.fetch("SELECT id, slug, name FROM facilities ORDER BY name") - - total_images_saved = 0 - - for i, fac in enumerate(facilities): - fac_id = fac['id'] - slug = fac['slug'] - name = fac['name'] - print(f"[{i+1}/{len(facilities)}] Sjekker: {name} ({slug})...") - - # Hent data fra WP med ?_embed for Γ₯ fΓ₯ tak i Utvalgt bilde lett - wp_url = f"https://teeoff.no/wp-json/wp/v2/golfbaner?slug={slug}&_embed" - wp_data_list = await fetch_json(wp_url) - - if not wp_data_list: - print(" ❌ Fant ikke anlegget i WordPress API.") - continue - - post = wp_data_list[0] - final_image_urls = [] - - # 1. SJEKK: "Utvalgt bilde" (Standard WordPress) - try: - embedded = post.get('_embedded', {}) - if 'wp:featuredmedia' in embedded and len(embedded['wp:featuredmedia']) > 0: - feat_media = embedded['wp:featuredmedia'][0] - feat_url = feat_media.get('source_url') - if feat_url: - final_image_urls.append(feat_url) - # print(f" -> Fant utvalgt bilde.") - except Exception as e: - print(f" ⚠️ Feil ved sjekk av utvalgt bilde: {e}") - - # 2. SJEKK: ACF Slides (Bildekarusell) - try: - acf = post.get('acf') or {} - slides_ids = acf.get('slides') - slide_urls = await fetch_media_urls_by_ids(slides_ids) - if slide_urls: - final_image_urls.extend(slide_urls) - # print(f" -> Fant {len(slide_urls)} bilder i slider.") - except Exception as e: - print(f" ⚠️ Feil ved sjekk av slides: {e}") - - # Fjern duplikater (hvis samme bilde er brukt begge steder) og bevar rekkefΓΈlgen - unique_urls = list(dict.fromkeys(final_image_urls)) - - # LAGRE I DATABASEN - if unique_urls: - sort_order = 0 - for url in unique_urls: - await conn.execute( - "INSERT INTO facility_images (facility_id, image_url, sort_order) VALUES ($1, $2, $3)", - fac_id, url, sort_order - ) - sort_order += 1 - print(f" βœ… Lagret {len(unique_urls)} unike bilder.") - total_images_saved += len(unique_urls) - else: - print(" ⚠️ Fant INGEN bilder for dette anlegget.") - - print(f"\nπŸŽ‰ FERDIG! Totalt {total_images_saved} bilder er nΓ₯ trygt lagret i galleriet.") - await conn.close() - -if __name__ == "__main__": - asyncio.run(run_robust_import()) diff --git a/kode_eksport_1/backend_import_nye_felter_py.txt b/kode_eksport_1/backend_import_nye_felter_py.txt deleted file mode 100644 index 51a9ac3..0000000 --- a/kode_eksport_1/backend_import_nye_felter_py.txt +++ /dev/null @@ -1,150 +0,0 @@ -import asyncio -import asyncpg -import requests -import json -import os -import re -from datetime import datetime -from dotenv import load_dotenv - -# Laster miljΓΈvariabler -load_dotenv() -DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") - -# Grunn-URL uten page-parameter -WP_API_BASE_URL = "https://teeoff.no/wp-json/wp/v2/golfbaner?per_page=100" - -def extract_price(text): - """Finner fΓΈrste hele tall i en tekst og returnerer det som integer.""" - if not text: - return None - clean_text = str(text).replace(" ", "").replace(".", "") - match = re.search(r'\d+', clean_text) - if match: - return int(match.group()) - return None - -def parse_date(date_string): - """ForsΓΈker Γ₯ konvertere ulike tekstformater for dato til et ekte Date-objekt.""" - if not date_string: - return None - ds = str(date_string).strip().lower() - - if ds in ["ukjent", "ikke oppgitt", "har ikke", ""]: - return None - - formats = ['%Y-%m-%d', '%d.%m.%Y', '%d/%m/%Y', '%Y%m%d', '%d.%m.%y'] - for fmt in formats: - try: - return datetime.strptime(ds, fmt).date() - except ValueError: - continue - return None - -def clean_jsonb(value): - """SΓΈrger for at vi ikke fyller databasen med 'Ikke oppgitt', men bruker tomme lister.""" - if not value or str(value).lower() in ["ikke oppgitt", "har ikke / ikke oppgitt"]: - return [] - - if isinstance(value, str): - return [{"beskrivelse": value}] - - if isinstance(value, list): - cleaned = [v for v in value if v and "ikke oppgitt" not in str(v).lower()] - return cleaned - - return value - -async def run_import(): - print("πŸ“‘ Henter anleggsdata fra WordPress (inkluderer paginering)...") - - all_data = [] - page = 1 - - # --- LØKKE SOM HENTER ALLE SIDER FRA WORDPRESS --- - while True: - url = f"{WP_API_BASE_URL}&page={page}" - print(f" -> Henter side {page}...") - response = requests.get(url) - - # Hvis vi fΓ₯r 400 Bad Request, betyr det at vi har nΓ₯dd forbi siste side - if response.status_code != 200: - break - - data = response.json() - if not data: - break - - all_data.extend(data) - page += 1 - - print(f"βœ… Fant totalt {len(all_data)} anlegg. Starter oppdatering av database...") - - conn = await asyncpg.connect(DB_URL) - success_count = 0 - - for item in all_data: - slug = item.get('slug') - acf = item.get('acf', {}) - - # Ekstraher og vask verdiene - golfpakker = clean_jsonb(acf.get('golfpakke')) - rabattert_greenfee = clean_jsonb(acf.get('rabattert_greenfee')) - - vtg_presentasjon = acf.get('vtg_presentasjon') or None - vtg_lenke = acf.get('lenke_til_kurssider') or None - vtg_pris = extract_price(acf.get('vtg_pris')) - vtg_kursdatoer = clean_jsonb(acf.get('kursdatoer')) - - slope_hovedbane = parse_date(acf.get('gyldig_til_og_med')) - slope_bane_to = parse_date(acf.get('gyldig_til_og_med_bane_to')) - - try: - # 1. Oppdater fasilitets-tabellen - await conn.execute(""" - UPDATE facilities - SET - golfpakker = $1::jsonb, - rabattert_greenfee = $2::jsonb, - vtg_presentasjon = $3, - vtg_lenke = $4, - vtg_pris = $5, - vtg_kursdatoer = $6::jsonb - WHERE slug = $7 - """, - json.dumps(golfpakker), - json.dumps(rabattert_greenfee), - vtg_presentasjon, - vtg_lenke, - vtg_pris, - json.dumps(vtg_kursdatoer), - slug) - - # 2. Oppdater utlΓΈpsdato pΓ₯ hovedbanen - if slope_hovedbane: - await conn.execute(""" - UPDATE courses - SET slope_valid_until = $1 - WHERE facility_id = (SELECT id FROM facilities WHERE slug = $2) - AND is_main_course = true - """, slope_hovedbane, slug) - - # 3. Oppdater utlΓΈpsdato pΓ₯ bane 2 - if slope_bane_to: - await conn.execute(""" - UPDATE courses - SET slope_valid_until = $1 - WHERE facility_id = (SELECT id FROM facilities WHERE slug = $2) - AND is_main_course = false - """, slope_bane_to, slug) - - success_count += 1 - - except Exception as e: - print(f" ❌ Feil ved oppdatering av {slug}: {e}") - - await conn.close() - print(f"\nπŸŽ‰ KjΓΈring fullfΓΈrt! MΓ₯lrettet import for {success_count} anlegg er lagret.") - -if __name__ == "__main__": - asyncio.run(run_import()) \ No newline at end of file diff --git a/kode_eksport_1/backend_import_urls_py.txt b/kode_eksport_1/backend_import_urls_py.txt deleted file mode 100644 index f1fa917..0000000 --- a/kode_eksport_1/backend_import_urls_py.txt +++ /dev/null @@ -1,101 +0,0 @@ -""" -TEE OFF - AUTOMATISK URL-IMPORTØR ---------------------------------------------------------------------------- -Leser tekstfiler med lenker og forsΓΈker Γ₯ matche dem mot eksisterende -golfanlegg i databasen basert pΓ₯ domenenavn. ---------------------------------------------------------------------------- -""" - -import asyncio -import asyncpg -import os -from urllib.parse import urlparse - -DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") - -# Hvilke filer vi skal lese, og hvilke databasefelt de tilhΓΈrer -FILES_TO_IMPORT = { - "Medlemsskap.txt": "medlemskap_url", - "GreenFee.txt": "greenfee_url", - "VtG.txt": "vtg_lenke" -} - -def extract_domain(url: str) -> str: - """Henter ut hoveddomenet (f.eks. 'tyrifjord-golfklubb.no') fra en URL.""" - try: - domain = urlparse(url.strip()).netloc.lower() - if domain.startswith("www."): - domain = domain[4:] - return domain - except: - return "" - -async def run_import(): - print("πŸš€ Starter URL-importΓΈr...") - conn = await asyncpg.connect(DB_URL) - - try: - # Hent alle eksisterende anlegg og deres domener - facilities = await conn.fetch("SELECT id, name, website_url, scrape_status_url FROM facilities") - - # Bygg en ordbok: { 'domenenavn.no': facility_id } for superraskt oppslag - domain_map = {} - for f in facilities: - # PrΓΈv Γ₯ hente domene fra website_url - if f['website_url']: - domain = extract_domain(f['website_url']) - if domain: domain_map[domain] = f['id'] - # PrΓΈv ogsΓ₯ scrape_status_url for sikkerhets skyld - if f['scrape_status_url']: - domain = extract_domain(f['scrape_status_url']) - if domain: domain_map[domain] = f['id'] - - print(f"πŸ“‹ Fant {len(domain_map)} unike domener i databasen.") - - # GΓ₯ gjennom fil for fil - for filename, db_field in FILES_TO_IMPORT.items(): - print(f"\n▢️ BEHANDLER: {filename} -> Setter felt: {db_field}") - - if not os.path.exists(filename): - print(f" ⚠️ Filen '{filename}' ble ikke funnet. Hopper over.") - continue - - with open(filename, 'r', encoding='utf-8') as file: - lines = [line.strip() for line in file.readlines() if line.strip()] - - matched_count = 0 - unmatched = [] - - for line in lines: - # Hvis det er flere URL-er pΓ₯ samme linje separert med komma, - # matcher vi basert pΓ₯ den FØRSTE URL-en. - first_url = line.split(',')[0].strip() - domain = extract_domain(first_url) - - # Hvis vi fant en match i databasen! - if domain in domain_map: - fac_id = domain_map[domain] - - # Oppdater databasen med HELE linjen (for Γ₯ bevare ev. komma-lenker) - await conn.execute(f""" - UPDATE facilities - SET {db_field} = $1 - WHERE id = $2 - """, line, fac_id) - matched_count += 1 - else: - unmatched.append(line) - - print(f" βœ… Matchet og oppdatert {matched_count} anlegg.") - - if unmatched: - print(f" ❌ FΓΈlgende {len(unmatched)} URL-er fant ingen match i databasen og mΓ₯ legges inn manuelt:") - for url in unmatched: - print(f" - {url}") - - finally: - await conn.close() - print("\n🏁 Import fullfΓΈrt!") - -if __name__ == "__main__": - asyncio.run(run_import()) \ No newline at end of file diff --git a/kode_eksport_1/backend_import_wp_py.txt b/kode_eksport_1/backend_import_wp_py.txt deleted file mode 100644 index 3eac73e..0000000 --- a/kode_eksport_1/backend_import_wp_py.txt +++ /dev/null @@ -1,157 +0,0 @@ -import asyncio, asyncpg, urllib.request, json, re, os, requests - -# --- KONFIGURASJON --- -DB_URL = "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff" -WP_API_URL = "https://teeoff.no/wp-json/wp/v2/golfbaner?per_page=100&_embed" -MEDIA_ENDPOINT = "https://teeoff.no/wp-json/wp/v2/media" -MEDIA_DIR = "./public/media" - -os.makedirs(MEDIA_DIR, exist_ok=True) -media_cache = {} - -def get_url_from_id(media_id): - if not media_id or not isinstance(media_id, int): return None - if media_id in media_cache: return media_cache[media_id] - try: - resp = requests.get(f"{MEDIA_ENDPOINT}/{media_id}", timeout=10) - if resp.status_code == 200: - url = resp.json().get('source_url') - media_cache[media_id] = url - return url - except: return None - -def download_media(url, slug, prefix): - if not isinstance(url, str) or not url: return None - clean_url = url.replace("https:///", "https://").replace("http:///", "http://") - if "teeoff.no" not in clean_url: return clean_url - try: - ext = clean_url.split('.')[-1].split('?')[0].lower() - if len(ext) > 4 or len(ext) < 3: ext = "jpg" - filename = f"{prefix}_{slug}.{ext}" - filepath = os.path.join(MEDIA_DIR, filename) - if os.path.exists(filepath): return f"/media/{filename}" - response = requests.get(clean_url, timeout=15) - if response.status_code == 200: - with open(filepath, 'wb') as f: f.write(response.content) - return f"/media/{filename}" - except: pass - return None - -def decode_html(text): - if not text: return "" - return str(text).replace('&', '&').replace('&', '&').replace(' ', ' ').strip() - -def parse_int(val): - if val is None or val == '': return None - try: - nums = re.findall(r'\d+', str(val)) - return int(nums[0]) if nums else None - except: return None - -def extract_url(val): - if isinstance(val, dict): return val.get('url') - if isinstance(val, str): return val - return None - -async def run_master_import(): - print("πŸš€ Starter MASTER IMPORT v9.2 (Robust datakonvertering & Banetype)...") - conn = await asyncpg.connect(DB_URL) - - # TΓΈmmer kun courses og holes (hjelpetabeller) - await conn.execute("TRUNCATE courses, holes RESTART IDENTITY CASCADE;") - - page = 1 - while True: - try: - req = urllib.request.Request(f"{WP_API_URL}&page={page}", headers={'User-Agent': 'TeeOff-V9.2'}) - with urllib.request.urlopen(req) as response: - data = json.loads(response.read().decode()) - except: break - if not data: break - - for post in data: - acf = post.get('acf', {}) - slug = post['slug'] - name = decode_html(post.get('title', {}).get('rendered', '')) - print(f"πŸ“¦ Mapper {name}...") - - # Media & Identifiers - local_main_img = download_media(post.get('_embedded', {}).get('wp:featuredmedia', [{}])[0].get('source_url'), slug, "main") - local_logo = download_media(get_url_from_id(acf.get('logo')) if isinstance(acf.get('logo'), int) else extract_url(acf.get('logo')), slug, "logo") - - # Galleri - slides = acf.get('slides') or [] - local_gallery = [download_media(get_url_from_id(s) if isinstance(s, int) else extract_url(s), f"{slug}_{i}", "slide") for i, s in enumerate(slides)] - local_gallery = [url for url in local_gallery if url] - - # Golfbox - booking_id = acf.get('golfbox_booking_id') - gb_booking_url = f"http://www.golfbox.no/site/system/redirect.asp?locale=nb_NO&rUrl=%2Fsite%2Fressources%2Fbooking%2Fgrid.asp%3FRessource_GUID%3D%{{{str(booking_id).strip().replace('{','').replace('}','')}}}" if booking_id else None - - # --- UPSERT FACILITY --- - # Merk: $16 (status_updated_at) pakkes nΓ₯ inn i TO_DATE for Γ₯ unngΓ₯ krasj - await conn.execute(''' - INSERT INTO facilities ( - name, slug, description, address, city, county, established_year, season, - email, phone, website_url, image_url, logo_url, video_url, - amenities, status_updated_at, gallery, banetype, - ngf_number, golfbox_club_id, golfbox_booking_url, - facebook_url, instagram_url, baneguide_url, flyfoto_url, - golfbox_tournament_url, footnote, social_links, webcam_url, - weather_url, architect, - navn_standard_medlemskap, standard_medlemskap, standard_medlemskap_kommentarer, - navn_rimeligste_alternativ, rimeligste_alternativ, rimeligste_alternativ_kommentarer, - medlemskap_url - ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15::jsonb, - TO_DATE(NULLIF($16, ''), 'YYYYMMDD'), - $17::jsonb, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28::jsonb, - $29, $30, $31, $32, $33, $34, $35, $36, $37, $38 - ) - ON CONFLICT (slug) DO UPDATE SET - name = EXCLUDED.name, - description = EXCLUDED.description, - address = EXCLUDED.address, - city = EXCLUDED.city, - phone = EXCLUDED.phone, - email = EXCLUDED.email, - website_url = EXCLUDED.website_url, - image_url = EXCLUDED.image_url, - logo_url = EXCLUDED.logo_url, - amenities = EXCLUDED.amenities, - gallery = EXCLUDED.gallery, - status_updated_at = EXCLUDED.status_updated_at, - banetype = EXCLUDED.banetype, - architect = EXCLUDED.architect - ''', name, slug, decode_html(acf.get('beskrivelse')), acf.get('gateadresse'), acf.get('postnummer_og_poststed'), acf.get('fylke'), parse_int(acf.get('byggear')), acf.get('sesong'), acf.get('e-post'), acf.get('telefon'), extract_url(acf.get('hjemmeside')), local_main_img, local_logo, None, json.dumps({"drivingrange": decode_html(acf.get("drivingrange")), "treningsgreen": decode_html(acf.get("treningsgreen")), "proshop": decode_html(acf.get("proshop")), "kafe": decode_html(acf.get("kafe")), "bilutleie": decode_html(acf.get("bilutleie")), "kolleutleie": decode_html(acf.get("kolleutleie")), "pro": decode_html(acf.get("pro")), "simulator": decode_html(acf.get("golfsimulator")), "antall_hull": decode_html(acf.get("antall_hull"))}), - acf.get('dato_for_oppdatert_status'), # $16 - json.dumps(local_gallery), decode_html(acf.get('banetype')), - parse_int(acf.get('klubbnummer_norges_golfforbund')), parse_int(acf.get('klubbnummer_golfbox')), gb_booking_url, extract_url(acf.get('facebook_url')), extract_url(acf.get('instagram_url')), extract_url(acf.get('baneguide')), extract_url(acf.get('flyfoto')), extract_url(acf.get('golfbox')), decode_html(acf.get('fotnote')), json.dumps(acf.get('sosiale_lenker') or []), decode_html(acf.get('webkamera')), extract_url(acf.get('varmelding_yr')), decode_html(acf.get('arkitekt')), decode_html(acf.get('navn_standard_medlemskap')), parse_int(acf.get('standard_medlemskap')), decode_html(acf.get('standard_medlemskap_kommentarer')), decode_html(acf.get('navn_rimeligste_alternativ')), parse_int(acf.get('rimeligste_alternativ')), decode_html(acf.get('rimeligste_alternativ_kommentarer')), extract_url(acf.get('medlemskap_url'))) - - fac_id = (await conn.fetchrow("SELECT id FROM facilities WHERE slug = $1", slug))['id'] - - # Baner og Hull - fac_main_len = 0 - for suffix in ['', '_bane_to']: - c_name = acf.get('navn_pa_hovedbane' if suffix == '' else 'navn_pa_sekundar_bane') or ('Hovedbanen' if suffix == '' else 'Bane 2') - status = acf.get('banestatus' if suffix == '' else 'banestatus_sekundar_bane') - if suffix == '_bane_to' and (status == 'finnes_ingen_bane_to' or not parse_int(acf.get('hull_1_par_bane_to'))): continue - course_id = await conn.fetchval('INSERT INTO courses (facility_id, name, status, par, is_main_course, tee_boxes, architect) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id', fac_id, c_name, status, parse_int(acf.get('totalt_par' if suffix == '' else 'totalt_par_bane_to')), (suffix == ''), json.dumps({"herrer": acf.get(f"utslag_herrer{suffix}"), "damer": acf.get(f"utslag_damer{suffix}")}), decode_html(acf.get('arkitekt'))) - curr_len = 0 - for h_num in range(1, 19): - p = parse_int(acf.get(f'hull_{h_num}_par{suffix}')) - if p: - idx = parse_int(acf.get(f'hull_{h_num}_index{suffix}')) - lens = {k: parse_int(acf.get(f'{k}_hull_{h_num}{suffix}')) for k in ['lengst', 'lang', 'mellomlang', 'mellomkort', 'kort', 'kortest']} - curr_len += (lens['lengst'] or 0) - await conn.execute('INSERT INTO holes (course_id, hole_number, par, hcp_index, lengths) VALUES ($1, $2, $3, $4, $5::jsonb)', course_id, h_num, p, idx, json.dumps(lens)) - await conn.execute("UPDATE courses SET length_meters = $1 WHERE id = $2", curr_len, course_id) - if suffix == '': fac_main_len = curr_len - await conn.execute("UPDATE facilities SET length_meters = $1 WHERE id = $2", fac_main_len, fac_id) - - page += 1 - await conn.close() - print("βœ… IMPORT FERDIG!") - -if __name__ == "__main__": - asyncio.run(run_master_import()) \ No newline at end of file diff --git a/kode_eksport_1/backend_main_py.txt b/kode_eksport_1/backend_main_py.txt deleted file mode 100644 index cfce462..0000000 --- a/kode_eksport_1/backend_main_py.txt +++ /dev/null @@ -1,638 +0,0 @@ -""" -TEE OFF BACKEND API v3.7.0 - KOBLET PΓ… FULL ADMIN REDIGERING ---------------------------------------------------------------------------- -REGEL 1: Bruk str (ikke string) for type-hinting. -REGEL 2: Inkluder alle subqueries for banestatus og hull-data. -REGEL 3: Robust JSON-parsing (format_row) for Γ₯ hindre Frontend-krasj. -REGEL 4: JWT-sesjoner lagres i HTTP-only cookies. -LOV: Aldri trunker eller slett logikk for "effektivitet". ---------------------------------------------------------------------------- -""" - -from fastapi import FastAPI, HTTPException, Response, Cookie, Depends, Request, BackgroundTasks -from fastapi.middleware.cors import CORSMiddleware -from contextlib import asynccontextmanager -import asyncpg -import json -import pyotp -import os -from datetime import datetime, date, timedelta -from jose import jwt, JWTError -from passlib.context import CryptContext -from dotenv import load_dotenv - -# NYE IMPORTER FOR ADMIN PANELET OG BAKGRUNNSJOBBER -from pydantic import BaseModel -from typing import Optional, List, Any -import subprocess - -load_dotenv() - -# --- KONFIGURASJON --- -DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") -SECRET_KEY = os.getenv("JWT_SECRET", "super_secret_change_this_in_production") -ALGORITHM = "HS256" - -pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto") - -# --- PYDANTIC MODELLER --- -class CourseStatusUpdate(BaseModel): - id: int - status: str - -class ScrapeSettingsUpdate(BaseModel): - scrape_method: Optional[str] = None - scrape_status_url: Optional[str] = None - scrape_status_selector: Optional[str] = None - ai_instruction: Optional[str] = None - courses: Optional[List[CourseStatusUpdate]] = [] - -# NY MODELL FOR Γ… TA IMOT IDER FOR SCRAPING -class ScrapeRunRequest(BaseModel): - facility_ids: List[int] - -class MembershipDraftApproval(BaseModel): - facility_id: int - navn_standard_medlemskap: Optional[str] = None - standard_medlemskap: Optional[int] = None - standard_medlemskap_kommentarer: Optional[str] = None - navn_rimeligste_alternativ: Optional[str] = None - rimeligste_alternativ: Optional[int] = None - -class BulkApprovalRequest(BaseModel): - approvals: List[MembershipDraftApproval] - -class QuickEditRequest(BaseModel): - field: str - value: str - -class GreenfeeApproval(BaseModel): - facility_id: int - greenfee: List[dict] - - -class VtgApproval(BaseModel): - facility_id: int - vtg_pris: int | None - vtg_beskrivelse: str | None - vtg_datoer: List[dict] | None - -class BulkVtgRequest(BaseModel): - approvals: List[VtgApproval] -# --- FUNKSJONER --- -def format_row(row): - """ - Vasker data fra databasen: - 1. Konverterer datoer til ISO-format. - 2. Tvinger tekst-JSON (stringified JSON) over til ekte Python objekter/lister. - 3. Sikrer at lister og objekter aldri er None for Γ₯ hindre Frontend-krasj. - """ - if row is None: - return None - - d = dict(row) - - for key in ['status_updated_at', 'created_at', 'slope_valid_until', 'membership_updated_at']: - if isinstance(d.get(key), (date, datetime)): - d[key] = d[key].isoformat() - - json_list_fields = [ - 'course_statuses', 'courses', 'gallery', 'greenfee', - 'faqs', 'shotzoom', 'social_links', 'holes', 'golfpakker', 'cooperating_clubs', 'vtg_datoer' - ] - json_dict_fields = [ - 'amenities', 'vtg', 'nsg_data', 'golfamore_data', 'membership_draft' - ] - - for field in json_list_fields: - if field in d: - val = d[field] - if val is None: - d[field] = [] - elif isinstance(val, str): - try: - d[field] = json.loads(val) - except: - d[field] = [] - elif not isinstance(val, list): - d[field] = [] - - for field in json_dict_fields: - if field in d: - val = d[field] - if val is None: - d[field] = {} - elif isinstance(val, str): - try: - d[field] = json.loads(val) - except: - d[field] = {} - elif not isinstance(val, dict): - d[field] = {} - - return d - -# --- BAKGRUNNSARBEIDER: FUNKSJON SOM KJØRER SKRAPEREN I BAKGRUNNEN --- -def run_scrape_worker(facility_ids: List[int]): - """ - KjΓΈrer selve skraping-scriptet i bakgrunnen. - Slik kan frontenden fΓ₯ et umiddelbart svar, mens skraperen jobber. - """ - print(f"πŸ”„ STARTER BAKGRUNNSSKRAPING FOR FØLGENDE IDER: {facility_ids}") - - try: - ids_arg = ",".join(map(str, facility_ids)) - - # NYTT: Bruker "python -u" for LIVE logging, og fjerner "> /dev/null 2>&1" - command = f"python -u scrape_status.py --ids {ids_arg}" - - subprocess.run(command, shell=True, check=True) - - print(f"βœ… BAKGRUNNSSKRAPING FULLFØRT FOR IDER: {facility_ids}") - except subprocess.CalledProcessError as e: - print(f"❌ FEIL UNDER BAKGRUNNSSKRAPING: {e}") - except Exception as e: - print(f"πŸ”₯ UFORUTSETT FEIL UNDER BAKGRUNNSSKRAPING: {e}") - -def run_membership_worker(facility_ids: List[int]): - """KjΓΈrer medlemskap-skraperen i bakgrunnen.""" - print(f"πŸ”„ STARTER MEDLEMSKAP-SKRAPING FOR IDER: {facility_ids}") - try: - ids_arg = ",".join(map(str, facility_ids)) - command = f"python -u scrape_membership.py --ids {ids_arg}" - subprocess.run(command, shell=True, check=True) - print(f"βœ… MEDLEMSKAP-SKRAPING FULLFØRT FOR IDER: {facility_ids}") - except Exception as e: - print(f"πŸ”₯ FEIL UNDER MEDLEMSKAP-SKRAPING: {e}") - - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Opprett database-pool ved start - try: - print(f"πŸ“‘ ForsΓΈker Γ₯ koble til database pΓ₯: {DB_URL}") - app.state.pool = await asyncpg.create_pool( - DB_URL, - min_size=5, - max_size=20, - command_timeout=60 - ) - print("βœ… Database tilkoblet og pool opprettet") - except Exception as e: - print(f"❌ Databasefeil under oppstart: {e}") - raise e - yield - # Lukk pool ved avslutning - await app.state.pool.close() - -app = FastAPI(title="TeeOff API v3.7.0", lifespan=lifespan) - -# CORS - Tillater bΓ₯de lokal utvikling og produksjonsdomene -app.add_middleware( - CORSMiddleware, - allow_origins=[ - "https://nye.teeoff.no", - "http://nye.teeoff.no", - "http://localhost:3000" - ], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# --- AUTH ENDPOINTS --- - -@app.post("/api/auth/login") -async def login(data: dict): - """Steg 1: Sjekk passord og returner temp_token for 2FA.""" - print(f"πŸ” Loggin-forsΓΈk for: {data.get('username')}") - - async with app.state.pool.acquire() as conn: - admin = await conn.fetchrow( - "SELECT * FROM admins WHERE username = $1 OR email = $1", - data.get('username') - ) - - if not admin: - print(" - ❌ Bruker ikke funnet i databasen") - raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord") - - h = admin['password_hash'] - print(f" - Verifiserer hash i DB (starter med: {h[:20]}...)") - - try: - is_valid = pwd_context.verify(data.get('password'), h) - except Exception as e: - print(f" - πŸ”₯ FEIL VED LESING AV HASH: {e}") - raise HTTPException(status_code=500, detail="Internt problem med passord-format") - - if not is_valid: - print(" - ❌ Passordet samsvarer ikke med hashen") - raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord") - - temp_token = jwt.encode( - {"sub": admin['username'], "partial": True, "exp": datetime.utcnow() + timedelta(minutes=5)}, - SECRET_KEY, algorithm=ALGORITHM - ) - print(" - βœ… Steg 1 fullfΓΈrt. Temp-token generert.") - return {"step": "2fa", "temp_token": temp_token} - -@app.post("/api/auth/verify-2fa") -async def verify_2fa(data: dict, response: Response): - """Steg 2: Verifiser TOTP-kode og sett session cookie.""" - try: - payload = jwt.decode(data.get('temp_token'), SECRET_KEY, algorithms=[ALGORITHM]) - if not payload.get("partial"): - raise JWTError() - username = payload.get("sub") - except JWTError: - raise HTTPException(status_code=401, detail="Sesjonen har utlΓΈpt eller er ugyldig") - - async with app.state.pool.acquire() as conn: - admin = await conn.fetchrow("SELECT otp_secret FROM admins WHERE username = $1", username) - - totp = pyotp.TOTP(admin['otp_secret']) - if not totp.verify(data.get('code')): - print(f" - ❌ Feil 2FA-kode oppgitt for {username}") - raise HTTPException(status_code=401, detail="Feil 2FA-kode") - - final_token = jwt.encode( - {"sub": username, "exp": datetime.utcnow() + timedelta(hours=12)}, - SECRET_KEY, algorithm=ALGORITHM - ) - - # Sett som HTTP-only cookie - response.set_cookie( - key="admin_session", - value=final_token, - httponly=True, - samesite="lax", - secure=False # Sett til True i produksjon (HTTPS) - ) - return {"status": "success"} - -# --- DATA ENDPOINTS --- - -@app.get("/api/facilities") -async def get_facilities(): - """Henter alle golfanlegg med aggregert banestatus for forsiden.""" - async with app.state.pool.acquire() as conn: - rows = await conn.fetch(""" - SELECT f.*, ( - 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 - FROM facilities f - ORDER BY f.name ASC - """) - return [format_row(row) for row in rows] - -@app.get("/api/facilities/{slug}") -async def get_facility(slug: str): - """Henter detaljer for ett spesifikt golfanlegg inkludert alle baner og hull.""" - async with app.state.pool.acquire() as conn: - row = await conn.fetchrow(""" - SELECT f.*, ( - SELECT jsonb_agg(c_data) FROM ( - SELECT c.*, ( - SELECT jsonb_agg(h_data ORDER BY h_data.hole_number ASC) - FROM (SELECT * FROM holes WHERE course_id = c.id) h_data - ) as holes - FROM courses c - WHERE c.facility_id = f.id - AND (c.is_main_course = true OR (c.status NOT IN ('finnes_ingen_bane_to', 'ukjent'))) - ORDER BY c.is_main_course DESC, c.id ASC - ) c_data - ) as courses - FROM facilities f WHERE f.slug = $1 - """, slug) - - if not row: - raise HTTPException(status_code=404, detail="Golfanlegget ble ikke funnet") - - return format_row(row) - -# --- ADMIN ENDPOINTS --- - -@app.patch("/api/admin/facilities/{facility_id}/scrape-settings") -async def update_scrape_settings(facility_id: int, settings: ScrapeSettingsUpdate): - """Oppdaterer hvordan et anlegg skal skrapes (f.eks. slΓ₯ pΓ₯ Gemini AI eller bytte URL).""" - async with app.state.pool.acquire() as conn: - try: - # Sjekk fΓΈrst at anlegget eksisterer - facility = await conn.fetchrow("SELECT id FROM facilities WHERE id = $1", facility_id) - if not facility: - raise HTTPException(status_code=404, detail="Anlegget finnes ikke.") - - # Oppdater verdiene i databasen inkludert AI instruks - await conn.execute(""" - UPDATE facilities - SET scrape_method = $1, - scrape_status_url = $2, - scrape_status_selector = $3, - ai_instruction = $4 - WHERE id = $5 - """, - settings.scrape_method, - settings.scrape_status_url, - settings.scrape_status_selector, - settings.ai_instruction, - facility_id) - - # Hvis metoden er manuell, tvinger vi gjennom de nye banestatusene direkte - if settings.scrape_method == 'manual' and settings.courses: - for c in settings.courses: - await conn.execute("UPDATE courses SET status = $1 WHERE id = $2", c.status, c.id) - - return {"status": "success", "message": f"Skrapeinnstillinger for anlegg ID {facility_id} ble oppdatert."} - - except Exception as e: - if isinstance(e, HTTPException): - raise e - raise HTTPException(status_code=500, detail=str(e)) - -# --- NYTT ADMIN ENDPOINT FOR FULL OPPDATERING (JSON-EDITOR) --- -@app.put("/api/admin/facilities/{facility_id}/full") -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() - - # Felter som er trygge Γ₯ oppdatere manuelt pΓ₯ anlegget - allowed_fields = [ - 'name', 'description', 'established_year', 'season', 'banetype', 'architect', 'length_meters', - 'address', 'zipcode', 'city', 'county', 'lat', 'lng', - 'email', 'phone', 'website_url', 'golfbox_booking_url', 'golfbox_tournament_url', - 'weather_url', 'webcam_url', 'video_url', 'baneguide_url', 'flyfoto_url', - 'amenities', 'greenfee', 'golfpakker', 'rabattert_greenfee', - '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', - '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' - ] - - update_data = {k: v for k, v in data.items() if k in allowed_fields} - - async with app.state.pool.acquire() as conn: - async with conn.transaction(): # Sikrer at alt lagres samlet - - # 1. OPPDATER ANLEGG (FACILITIES) - if update_data: - set_clauses = [] - values = [] - for i, (k, v) in enumerate(update_data.items(), 1): - if isinstance(v, (dict, list)): - set_clauses.append(f"{k} = ${i}::jsonb") - values.append(json.dumps(v)) - else: - set_clauses.append(f"{k} = ${i}") - values.append(v) - - values.append(facility_id) - query = f"UPDATE facilities SET {', '.join(set_clauses)} WHERE id = ${len(values)}" - await conn.execute(query, *values) - - # 2. OPPDATER BANER (COURSES) OG HULL (HOLES) - courses = data.get('courses', []) - for course in courses: - course_id = course.get('id') - if course_id: - # Rens datoformat for PostgreSQL (hΓ₯ndterer Next.js date input) - valid_until = course.get('slope_valid_until') - if valid_until == "" or valid_until is None: - valid_until = None - - await conn.execute(""" - UPDATE courses - SET name=$1, par=$2, length_meters=$3, architect=$4, - status=$5, is_main_course=$6, tee_boxes=$7::jsonb, - slope_valid_until=$8 - WHERE id=$9 AND facility_id=$10 - """, - 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) - - # 3. OPPDATER HULL PΓ… BANEN (HOLES) - holes = course.get('holes', []) - for hole in holes: - hole_id = hole.get('id') - if hole_id: - await conn.execute(""" - UPDATE holes - SET par=$1, hcp_index=$2, lengths=$3::jsonb - WHERE id=$4 AND course_id=$5 - """, - hole.get('par'), hole.get('hcp_index'), - json.dumps(hole.get('lengths', {})), hole_id, course_id) - - return {"status": "success", "message": "Anlegg, baner og scorekort ble oppdatert."} - -# --- NYTT ADMIN ENDPOINT: KJØRER SKRAPEREN FOR VALGTE IDER --- -@app.post("/api/admin/run-scraper") -async def run_scraper_endpoint(request: ScrapeRunRequest, background_tasks: BackgroundTasks): - """ - Tar imot IDer for skraping, og starter en bakgrunnsjobb. - Gir et umiddelbart svar tilbake til frontenden slik at den slipper Γ₯ vente. - """ - if not request.facility_ids: - raise HTTPException(status_code=400, detail="Ingen anleggs-IDer ble oppgitt.") - - print(f"πŸ“‘ API mottok forespΓΈrsel om Γ₯ kjΓΈre skraping for IDer: {request.facility_ids}") - - background_tasks.add_task(run_scrape_worker, request.facility_ids) - - return {"status": "queued", "message": f"Skraping for {len(request.facility_ids)} anlegg ble lagt i kΓΈ."} - -@app.post("/api/admin/run-membership-scraper") -async def run_membership_scraper_endpoint(request: ScrapeRunRequest, background_tasks: BackgroundTasks): - """Tar imot IDer for medlemskapsskraping og legger jobben i kΓΈ.""" - if not request.facility_ids: - raise HTTPException(status_code=400, detail="Ingen anleggs-IDer ble oppgitt.") - - print(f"πŸ“‘ API mottok forespΓΈrsel om medlemskapsskraping for IDer: {request.facility_ids}") - background_tasks.add_task(run_membership_worker, request.facility_ids) - - return {"status": "queued", "message": f"Medlemskapsskraping for {len(request.facility_ids)} anlegg ble lagt i kΓΈ."} - -@app.get("/api/health") -async def health_check(): - """Enkel sjekk for Γ₯ se at API og DB lever.""" - try: - async with app.state.pool.acquire() as conn: - await conn.execute("SELECT 1") - return {"status": "healthy", "database": "connected"} - except Exception as e: - return {"status": "unhealthy", "error": str(e)} - -# --- MEDLEMSKAP "VASKERI" ENDEPUNKTER --- - -@app.get("/api/admin/membership/drafts") -async def get_membership_drafts(): - """Henter alle anlegg som har et ventende forslag fra AI-skraperen.""" - async with app.state.pool.acquire() as conn: - rows = await conn.fetch(""" - SELECT id, name, slug, medlemskap_url, - navn_standard_medlemskap, standard_medlemskap, - navn_rimeligste_alternativ, rimeligste_alternativ, - membership_draft - FROM facilities - WHERE membership_draft IS NOT NULL - AND membership_draft::text != '{}' - ORDER BY name ASC - """) - return [format_row(row) for row in rows] - -@app.post("/api/admin/membership/approve-bulk") -async def approve_membership_bulk(request: BulkApprovalRequest): - """Godkjenner AI-forslag, setter oppdatert-dato og sletter utkastet.""" - async with app.state.pool.acquire() as conn: - async with conn.transaction(): - for approval in request.approvals: - await conn.execute(""" - UPDATE facilities - SET navn_standard_medlemskap = $1, - standard_medlemskap = $2, - standard_medlemskap_kommentarer = $3, - navn_rimeligste_alternativ = $4, - rimeligste_alternativ = $5, - membership_updated_at = NOW(), - membership_draft = NULL - WHERE id = $6 - """, - approval.navn_standard_medlemskap, - approval.standard_medlemskap, - approval.standard_medlemskap_kommentarer, - approval.navn_rimeligste_alternativ, - approval.rimeligste_alternativ, - approval.facility_id) - return {"status": "success", "message": f"{len(request.approvals)} anlegg ble oppdatert med nye priser!"} - -@app.patch("/api/admin/facilities/{facility_id}/quick-edit") -async def quick_edit_facility(facility_id: int, request: QuickEditRequest): - """Lyn-redigering av enkle URL-felter fra admin-dashbordet.""" - # Sikkerhet: Tillat KUN disse tre feltene for hurtigredigering - allowed_fields = ['scrape_status_url', 'medlemskap_url', 'scrape_status_selector'] - if request.field not in allowed_fields: - raise HTTPException(status_code=400, detail="Ugyldig felt for hurtigredigering.") - - async with app.state.pool.acquire() as conn: - # F-string her er trygt fordi request.field er sjekket mot allowed_fields-listen - await conn.execute(f"UPDATE facilities SET {request.field} = $1 WHERE id = $2", - request.value, facility_id) - return {"status": "success"} - -# --- GREENFEE "VASKERI" ENDEPUNKTER --- - -@app.get("/api/admin/greenfee/drafts") -async def get_greenfee_drafts(): - """Henter alle anlegg som har et ventende greenfee-forslag fra AI-skraperen.""" - async with app.state.pool.acquire() as conn: - rows = await conn.fetch(""" - SELECT id, name, slug, greenfee_url, greenfee, greenfee_draft - FROM facilities - WHERE greenfee_draft IS NOT NULL - AND greenfee_draft::text != '{}' - ORDER BY name ASC - """) - return [format_row(row) for row in rows] - -class BulkGreenfeeRequest(BaseModel): - approvals: List[GreenfeeApproval] - -@app.post("/api/admin/greenfee/approve-bulk") -async def approve_greenfee_bulk(request: BulkGreenfeeRequest): - """Godkjenner AI-forslag, setter oppdatert-dato og sletter utkastet.""" - async with app.state.pool.acquire() as conn: - async with conn.transaction(): - for approval in request.approvals: - await conn.execute(""" - UPDATE facilities - SET greenfee = $1::jsonb, - greenfee_updated_at = NOW(), - greenfee_draft = NULL - WHERE id = $2 - """, json.dumps(approval.greenfee), approval.facility_id) - return {"status": "success"} - -def run_greenfee_worker(facility_ids: List[int]): - """KjΓΈrer greenfee-skraperen i bakgrunnen.""" - print(f"πŸ”„ STARTER GREENFEE-SKRAPING FOR IDER: {facility_ids}") - try: - import subprocess - ids_arg = ",".join(map(str, facility_ids)) - command = f"python -u scrape_greenfee.py --ids {ids_arg}" - subprocess.run(command, shell=True, check=True) - print(f"βœ… GREENFEE-SKRAPING FULLFØRT FOR IDER: {facility_ids}") - except Exception as e: - print(f"πŸ”₯ FEIL UNDER GREENFEE-SKRAPING: {e}") - -@app.post("/api/admin/run-greenfee-scraper") -async def run_greenfee_scraper_endpoint(request: ScrapeRunRequest, background_tasks: BackgroundTasks): - """Tar imot IDer for greenfeeskraping og legger jobben i kΓΈ.""" - if not request.facility_ids: - raise HTTPException(status_code=400, detail="Ingen anleggs-IDer ble oppgitt.") - background_tasks.add_task(run_greenfee_worker, request.facility_ids) - return {"status": "queued", "message": "Skraping startet"} - -# --- VEIEN TIL GOLF (VTG) "VASKERI" ENDEPUNKTER --- - -@app.get("/api/admin/vtg/drafts") -async def get_vtg_drafts(): - """Henter alle anlegg som har et ventende VTG-forslag.""" - async with app.state.pool.acquire() as conn: - rows = await conn.fetch(""" - SELECT id, name, slug, vtg_lenke, vtg_pris, vtg_beskrivelse, vtg_datoer, vtg_draft - FROM facilities - WHERE vtg_draft IS NOT NULL - AND vtg_draft::text != '{}' - ORDER BY name ASC - """) - return [format_row(row) for row in rows] - -@app.post("/api/admin/vtg/approve-bulk") -async def approve_vtg_bulk(request: BulkVtgRequest): - """Godkjenner AI-forslag for VTG, setter oppdatert-dato og sletter utkastet.""" - async with app.state.pool.acquire() as conn: - async with conn.transaction(): - for approval in request.approvals: - datoer_json = json.dumps(approval.vtg_datoer) if approval.vtg_datoer is not None else '[]' - await conn.execute(""" - UPDATE facilities - SET vtg_pris = $1, - vtg_beskrivelse = $2, - vtg_datoer = $3::jsonb, - vtg_updated_at = NOW(), - vtg_draft = NULL - WHERE id = $4 - """, approval.vtg_pris, approval.vtg_beskrivelse, datoer_json, approval.facility_id) - return {"status": "success"} - -def run_vtg_worker(facility_ids: List[int]): - """KjΓΈrer VTG-skraperen i bakgrunnen.""" - print(f"πŸ”„ STARTER VTG-SKRAPING FOR IDER: {facility_ids}") - try: - import subprocess - ids_arg = ",".join(map(str, facility_ids)) - command = f"python -u scrape_vtg.py --ids {ids_arg}" - subprocess.run(command, shell=True, check=True) - print(f"βœ… VTG-SKRAPING FULLFØRT FOR IDER: {facility_ids}") - except Exception as e: - print(f"πŸ”₯ FEIL UNDER VTG-SKRAPING: {e}") - -@app.post("/api/admin/run-vtg-scraper") -async def run_vtg_scraper_endpoint(request: ScrapeRunRequest, background_tasks: BackgroundTasks): - """Tar imot IDer for VTG-skraping og legger jobben i kΓΈ.""" - if not request.facility_ids: - raise HTTPException(status_code=400, detail="Ingen anleggs-IDer ble oppgitt.") - background_tasks.add_task(run_vtg_worker, request.facility_ids) - return {"status": "queued", "message": "Skraping startet"} - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/kode_eksport_1/backend_scrape_golfamore1_3_py.txt b/kode_eksport_1/backend_scrape_golfamore1_3_py.txt deleted file mode 100644 index fd418e0..0000000 --- a/kode_eksport_1/backend_scrape_golfamore1_3_py.txt +++ /dev/null @@ -1,124 +0,0 @@ -import asyncio -import asyncpg -import json -import re - -DB_URL = "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff" - -# Data hentet direkte fra bildet du sendte -GOLFAMORE_DATA = { - "borre": "Kortet er gyldig hverdager (ikke helligdager), ikke uke 14, 19, 20, 21", - "nesfjellet": "Kortet er gyldig hverdager (ikke helligdager), ikke uke 27, 28, 29, 30", - "vradal": "Kortet er gyldig alle dager, ikke uke 28, 29, 30, 31", - "alta": "Kortet er gyldig alle dager", - "elverum": "Kortet er gyldig hverdager (ikke helligdager)", - "gronmo": "Kortet er gyldig alle dager", - "notteroy": "Kortet er gyldig hverdager (ikke helligdager), ikke uke 27, 28, 29, 30", - "roros": "Kortet er gyldig alle dager", - "stiklestad": "Kortet er gyldig alle dager", - "arendalomegn": "Kortet er gyldig alle dager, ikke uke 27, 28, 29, 30", - "northcape": "Kortet er gyldig alle dager", - "trysil": "Kortet er gyldig hverdager (ikke helligdager), ikke uke 28, 29, 30, 31", - "mork": "Kortet er gyldig hverdager (ikke helligdager)", - "norsjo": "Kortet er gyldig alle dager", - "ringerike": "Kortet er gyldig alle dager", - "stord": "Kortet er gyldig alle dager", - "sunnmore": "Kortet er gyldig alle dager", - "bodogolfparksalten": "Kortet er gyldig alle dager", - "drammen": "Kortet er gyldig alle dager", - "gjoviktoten": "Kortet er gyldig alle dager", - "grenlandomegn": "Kortet er gyldig hverdager (ikke helligdager), ikke uke 27, 28, 29, 30", - "nes09": "Kortet er gyldig alle dager, ikke uke 15, 16, 17, 18", - "romerike": "Kortet er gyldig alle dager", - "bamble": "Kortet er gyldig alle dager", - "bleik": "Kortet er gyldig alle dager", - "krokhol": "Kortet er gyldig alle dager", - "skjeberg": "Kortet er gyldig hverdager (ikke helligdager)", - "utsikten": "Kortet er gyldig hverdager (ikke helligdager), ikke uke 27, 28, 29, 30", - "eiker": "Kortet er gyldig alle dager", - "hafjell": "Kortet er gyldig alle dager", - "mandal": "Kortet er gyldig alle dager, ikke uke 27, 28, 29, 30", - "mjosen": "Kortet er gyldig alle dager", - "randsfjorden": "Kortet er gyldig alle dager", - "ski": "Kortet er gyldig alle dager", - "bjornefjorden": "Kortet er gyldig alle dager", - "sande": "Kortet er gyldig alle dager", - "haugesund": "Kortet er gyldig alle dager", - "midttroms": "Kortet er gyldig alle dager", - "skei": "Kortet er gyldig hverdager (ikke helligdager)", - "sorknes": "Kortet er gyldig alle dager", - "gjerdrum": "Kortet er gyldig alle dager", - "herdla": "Kortet er gyldig alle dager", - "hovden": "Kortet er gyldig alle dager", - "oppdal": "Kortet er gyldig alle dager", - "gjersjoen": "Kortet er gyldig alle dager", - "ogna": "Kortet er gyldig alle dager", - "tonsberg": "Kortet er gyldig alle dager", - "ullensaker": "Kortet er gyldig alle dager", - "hof": "Kortet er gyldig hverdager (ikke helligdager)", - "klabu": "Kortet er gyldig alle dager", - "hemsedal": "Kortet er gyldig alle dager", - "narvik": "Kortet er gyldig alle dager", - "norefjell": "Kortet er gyldig hverdager (ikke helligdager)", - "austratt": "Kortet er gyldig alle dager", - "hammerfest": "Kortet er gyldig alle dager", - "helgeland": "Kortet er gyldig alle dager", - "jaren": "Kortet er gyldig alle dager", - "namdal": "Kortet er gyldig alle dager", - "namsos": "Kortet er gyldig alle dager", - "nordfjord": "Kortet er gyldig alle dager", - "polarsirkelen": "Kortet er gyldig alle dager", - "sandnesbarheim": "Kortet er gyldig alle dager", - "steinkjer": "Kortet er gyldig alle dager", - "varanger": "Kortet er gyldig alle dager" -} - -def clean(text): - if not text: return "" - # Fjerner alt som ikke er bokstaver/tall for matching - s = text.lower().replace("golfklubb", "").replace("gk", "").replace(" og ", "").replace("&", "").strip() - return re.sub(r'[^a-z0-9]', '', s) - -async def update_golfamore(): - print("\nπŸš€ OPPDATERER GOLFAMORE FRA BILDE-DATA...") - conn = await asyncpg.connect(DB_URL) - facilities = await conn.fetch("SELECT id, name FROM facilities") - - # Lag et vasket map av bilde-dataen - image_data_clean = {clean(name): val for name, val in GOLFAMORE_DATA.items()} - - matches = 0 - for fac in facilities: - fac_id = fac['id'] - fac_name = fac['name'] - fac_clean = clean(fac_name) - - validity = None - # PrΓΈv eksakt match fΓΈrst - if fac_clean in image_data_clean: - validity = image_data_clean[fac_clean] - else: - # PrΓΈv delvis match (f.eks "Arendal" i "Arendal & Omegn") - for key, val in image_data_clean.items(): - if len(fac_clean) > 4 and (fac_clean in key or key in fac_clean): - validity = val - break - - if validity: - print(f"βœ… Match funnet: {fac_name}") - ga_data = {"validity": validity} - await conn.execute(""" - UPDATE facilities - SET golfamore = true, golfamore_data = $1 - WHERE id = $2 - """, json.dumps(ga_data), fac_id) - matches += 1 - else: - # Hvis den ikke er i listen fra bildet, sett til false - await conn.execute("UPDATE facilities SET golfamore = false, golfamore_data = '{}' WHERE id = $1", fac_id) - - await conn.close() - print(f"\nπŸŽ‰ Ferdig! {matches} baner ble oppdatert med Golfamore-info.") - -if __name__ == "__main__": - asyncio.run(update_golfamore()) diff --git a/kode_eksport_1/backend_scrape_greenfee_py.txt b/kode_eksport_1/backend_scrape_greenfee_py.txt deleted file mode 100644 index edf796f..0000000 --- a/kode_eksport_1/backend_scrape_greenfee_py.txt +++ /dev/null @@ -1,173 +0,0 @@ -""" -TEE OFF - GREENFEE-SKRAPER MED GEMINI AI ---------------------------------------------------------------------------- -Henter alle greenfee-varianter fra en (eller flere) URL-er og strukturerer -dem i en JSON-liste. Finner ogsΓ₯ avtaleklubber/vennskapsklubber. ---------------------------------------------------------------------------- -""" - -import asyncio -import asyncpg -import os -import json -import argparse -from bs4 import BeautifulSoup -from playwright.async_api import async_playwright -import google.generativeai as genai -from dotenv import load_dotenv - -load_dotenv() - -DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") -GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") - -if not GEMINI_API_KEY: - raise ValueError("🚨 GEMINI_API_KEY mangler i .env filen!") - -genai.configure(api_key=GEMINI_API_KEY) -model = genai.GenerativeModel('gemini-2.5-flash') - -async def fetch_page_text(url: str, browser) -> str: - url = url.strip() - if not url.startswith("http"): - return "" - - print(f" 🌐 Laster inn: {url}") - try: - page = await browser.new_page() - await page.goto(url, wait_until="domcontentloaded", timeout=15000) - html_content = await page.content() - await page.close() - - soup = BeautifulSoup(html_content, 'html.parser') - for script in soup(["script", "style", "nav", "footer", "header"]): - script.extract() - - return soup.get_text(separator=' ', strip=True) - except Exception as e: - print(f" ❌ Feil ved lasting av {url}: {e}") - return "" - -def analyze_greenfee_with_gemini(text: str, club_name: str) -> dict: - print(f" 🧠 Sender {len(text)} tegn til Gemini for greenfee-analyse...") - - prompt = f""" -Du er en ekspert pΓ₯ norske golfklubber og prissetting. -Din oppgave er Γ₯ lese teksten hentet fra nettsidene til "{club_name}" og hente ut TO ting: -1. ALLE varianter av greenfee-priser. -2. Navn pΓ₯ eventuelle vennskapsklubber/avtaleklubber (hvis nevnt). - -REGLER FOR GREENFEE: -- Trekk ut absolutt alle priskategorier du finner (f.eks. "Hverdag hΓΈysesong", "Helg fΓΈr kl 14", "Gjest av medlem", "9 hull kveld", osv.). -- Finn bΓ₯de voksenpris og juniorpris for hver kategori. -- HVIS juniorpris er oppgitt som en regel (f.eks. "Juniorer betaler halv pris" eller "50% rabatt for junior"), MΓ… du selv regne ut prisen og skrive inn heltallet. -- "banenavn": Bruk navnet pΓ₯ banen hvis det er spesifisert (f.eks. "18-hullsbanen", "Korthullsbanen"). Hvis ikke spesifisert, bruk "{club_name}". -- Priser SKAL vΓ¦re tall (integer). Sett pris til null (null) hvis den ikke finnes. - -REGLER FOR AVTALEKLUBBER: -- Let etter overskrifter som "Vennskapsklubber", "Avtaleklubber", "Gjestespill", "Samarbeidsklubber". -- Trekk ut kun navnene pΓ₯ klubbene i en liste (f.eks. ["Haga GK", "Oslo GK"]). La listen vΓ¦re tom hvis du ikke finner noen. - -TEKST FRA NETTSIDEN: -{text} - -OPPGAVE: -Returner KUN et gyldig JSON-objekt med nΓΈyaktig fΓΈlgende struktur: -{{ - "foreslatt_greenfee": [ - {{ - "banenavn": "Navn pΓ₯ banen", - "priskategori": "F.eks: Hverdag Gjest av Medlem", - "pris_voksne": 600, - "pris_junior": 300 - }} - ], - "foreslatt_avtaleklubber": [ - "Klubb 1 GK", - "Klubb 2 GK" - ], - "ai_begrunnelse": "Kort forklaring, f.eks: 'Fant et komplekst prissystem for hΓΈy/lavsesong. Regnet ut juniorpriser til 50% som angitt i teksten. Fant 3 samarbeidsklubber nederst.'" -}} -""" - - try: - response = model.generate_content(prompt) - raw_response = response.text.strip() - - if raw_response.startswith("```json"): - raw_response = raw_response[7:] - if raw_response.endswith("```"): - raw_response = raw_response[:-3] - - return json.loads(raw_response.strip()) - except Exception as e: - print(f" ❌ AI-analyse feilet: {e}") - return None - -async def run_greenfee_scraper(facility_ids=None): - print("πŸš€ Starter Greenfee-skraperen...") - conn = await asyncpg.connect(DB_URL) - - try: - query = "SELECT id, name, greenfee_url FROM facilities WHERE greenfee_url IS NOT NULL AND greenfee_url != ''" - if facility_ids: - query += f" AND id IN ({','.join(map(str, facility_ids))})" - - facilities = await conn.fetch(query) - print(f"πŸ“‹ Fant {len(facilities)} anlegg Γ₯ skrape.") - - async with async_playwright() as p: - browser = await p.chromium.launch(headless=True) - - for facility in facilities: - fac_id = facility['id'] - name = facility['name'] - urls_raw = facility['greenfee_url'] - - print(f"\n▢️ Behandler Greenfee for: {name} (ID: {fac_id})") - - urls = [u.strip() for u in urls_raw.split(',')] - combined_text = "" - - for idx, url in enumerate(urls, 1): - page_text = await fetch_page_text(url, browser) - if page_text: - combined_text += f"\n\n--- TEKST FRA SIDE {idx} ({url}) ---\n{page_text}" - - if len(combined_text) < 50: - print(" ⚠️ Fant for lite tekst, hopper over.") - continue - - draft_data = analyze_greenfee_with_gemini(combined_text[:25000], name) - - if not draft_data: - continue - - funnet_priser = len(draft_data.get('foreslatt_greenfee', [])) - funnet_klubber = len(draft_data.get('foreslatt_avtaleklubber', [])) - print(f" βœ… AI fant {funnet_priser} greenfee-varianter og {funnet_klubber} avtaleklubber.") - - await conn.execute(""" - UPDATE facilities - SET greenfee_draft = $1::jsonb - WHERE id = $2 - """, json.dumps(draft_data), fac_id) - - print(" πŸ’Ύ Greenfee-utkast lagret i databasen!") - - await browser.close() - - finally: - await conn.close() - print("\n🏁 Skraping fullfΓΈrt.") - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Skrap greenfeepriser via AI.") - parser.add_argument("--ids", type=str, help="Kommaseparert liste med facility IDs (eks: 1,5,12)") - args = parser.parse_args() - - ids_to_scrape = None - if args.ids: - ids_to_scrape = [int(x.strip()) for x in args.ids.split(",")] - - asyncio.run(run_greenfee_scraper(ids_to_scrape)) \ No newline at end of file diff --git a/kode_eksport_1/backend_scrape_membership_py.txt b/kode_eksport_1/backend_scrape_membership_py.txt deleted file mode 100644 index 2fb99fe..0000000 --- a/kode_eksport_1/backend_scrape_membership_py.txt +++ /dev/null @@ -1,168 +0,0 @@ -""" -TEE OFF - MEDLEMSKAPSSKRAPER MED GEMINI AI (MULTI-URL VERSJON) ---------------------------------------------------------------------------- -GΓ₯r til oppgitte medlemskaps-URLer (stΓΈtter flere URLer adskilt med komma), -henter ut tekst, og bruker Gemini til Γ₯ summere og finne 'Standard' og -'Rimeligste' medlemskap. ---------------------------------------------------------------------------- -""" - -import asyncio -import asyncpg -import os -import json -import argparse -from bs4 import BeautifulSoup -from playwright.async_api import async_playwright -import google.generativeai as genai -from dotenv import load_dotenv - -load_dotenv() - -DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") -GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") - -if not GEMINI_API_KEY: - raise ValueError("🚨 GEMINI_API_KEY mangler i .env filen!") - -genai.configure(api_key=GEMINI_API_KEY) -model = genai.GenerativeModel('gemini-2.5-flash') - -async def fetch_page_text(url: str, browser) -> str: - """Bruker Playwright for Γ₯ hente all synlig tekst fra EN nettside.""" - url = url.strip() - if not url.startswith("http"): - return "" - - print(f" 🌐 Laster inn: {url}") - try: - page = await browser.new_page() - await page.goto(url, wait_until="domcontentloaded", timeout=15000) - html_content = await page.content() - await page.close() - - soup = BeautifulSoup(html_content, 'html.parser') - for script in soup(["script", "style", "nav", "footer", "header"]): - script.extract() - - text = soup.get_text(separator=' ', strip=True) - return text - except Exception as e: - print(f" ❌ Feil ved lasting av {url}: {e}") - return "" - -def analyze_with_gemini(text: str, club_name: str) -> dict: - """Sender den kombinerte teksten til Gemini for Γ₯ trekke ut og evt. summere priser.""" - print(f" 🧠 Sender {len(text)} tegn til Gemini for analyse...") - - prompt = f""" -Du er en ekspert pΓ₯ norske golfklubber. Din oppgave er Γ₯ lese teksten hentet fra nettsidene til "{club_name}" og finne to spesifikke priser. - -VIKTIG REGEL OM NORSK GOLF: -Mange steder er "Klubbkontingent/Medlemskap" og "Spillerett/Γ…rskort" to forskjellige ting. -For Γ₯ fΓ₯ spille ubegrenset (Fritt spill) MΓ… man betale BEGGE DELER. Hvis du ser at prisene for kontingent og spillerett er oppgitt hver for seg, SKAL DU SUMMERE disse to summene og bruke totalen som "Standard pris". - -ALDERSPREMISS FOR BEGGE PRISER: -Vi forutsetter at personen som skal ha medlemskap er en VOKSEN GOLFER PΓ… MINST 35 Γ…R. Du mΓ₯ ALDRI velge priser som gjelder for barn, junior, ung voksen (f.eks. 20-29 Γ₯r), student eller senior/pensjonist. - -DEFINISJONER DU MΓ… FØLGE STRENGT: -1. "Standard medlemskap": Hva er TOTALPRISEN (inkludert evt. spillerett/Γ₯rskort) for en voksen person (35+ Γ₯r) for Γ₯ spille SΓ… MYE VEDKOMMENDE ØNSKER (Fritt spill) i Γ₯r? -2. "Rimeligste alternativ": Det absolutt billigste alternativet FOR EN VOKSEN PERSON (35+ Γ₯r) som gir medlemskap i klubben (golfkortet), forutsatt at man betaler greenfee for hver runde. (Ofte kalt Greenfeemedlem, Postkassemedlem, Fjernmedlem, eller kun "Klubbkontingent for voksne" uten spillerett). - -TEKST FRA NETTSIDEN(E): -{text} - -OPPGAVE: -Returner KUN et gyldig JSON-objekt med fΓΈlgende struktur: -{{ - "foreslatt_standard_navn": "Navn (eks: Hovedmedlem Voksen inkl. spillerett)", - "foreslatt_standard_pris": 1234, - "foreslatt_standard_kommentar": "Kort kommentar (eks: MΓ₯tte summere kontingent pΓ₯ 900 og Γ₯rskort pΓ₯ 5000)", - "foreslatt_rimeligste_navn": "Navn (eks: Greenfeemedlemskap Voksen)", - "foreslatt_rimeligste_pris": 500, - "ai_begrunnelse": "Kort forklaring pΓ₯ utregningen din." -}} -Merk: Prisene SKAL vΓ¦re tall (integer), ikke tekst. Sett til null hvis du ikke finner det. -""" - - try: - response = model.generate_content(prompt) - raw_response = response.text.strip() - - if raw_response.startswith("```json"): - raw_response = raw_response[7:] - if raw_response.endswith("```"): - raw_response = raw_response[:-3] - - return json.loads(raw_response.strip()) - except Exception as e: - print(f" ❌ AI-analyse feilet: {e}") - return None - -async def run_scraper(facility_ids=None): - print("πŸš€ Starter Medlemskaps-skraperen (StΓΈtter multi-URL)...") - conn = await asyncpg.connect(DB_URL) - - try: - query = "SELECT id, name, medlemskap_url FROM facilities WHERE medlemskap_url IS NOT NULL AND medlemskap_url != ''" - if facility_ids: - query += f" AND id IN ({','.join(map(str, facility_ids))})" - - facilities = await conn.fetch(query) - print(f"πŸ“‹ Fant {len(facilities)} anlegg Γ₯ skrape.") - - async with async_playwright() as p: - browser = await p.chromium.launch(headless=True) - - for facility in facilities: - fac_id = facility['id'] - name = facility['name'] - urls_raw = facility['medlemskap_url'] - - print(f"\n▢️ Behandler: {name} (ID: {fac_id})") - - # Sjekker om det er flere URL-er adskilt med komma - urls = [u.strip() for u in urls_raw.split(',')] - combined_text = "" - - for idx, url in enumerate(urls, 1): - page_text = await fetch_page_text(url, browser) - if page_text: - combined_text += f"\n\n--- TEKST FRA SIDE {idx} ({url}) ---\n{page_text}" - - if len(combined_text) < 50: - print(" ⚠️ Fant for lite tekst, hopper over.") - continue - - # Kutter teksten for Γ₯ ikke overbelaste Gemini (ca 25000 tegn maks) - draft_data = analyze_with_gemini(combined_text[:25000], name) - - if not draft_data: - continue - - print(f" βœ… AI foreslΓ₯r: Standard: {draft_data.get('foreslatt_standard_pris')} | Rimeligste: {draft_data.get('foreslatt_rimeligste_pris')}") - - await conn.execute(""" - UPDATE facilities - SET membership_draft = $1::jsonb - WHERE id = $2 - """, json.dumps(draft_data), fac_id) - - print(" πŸ’Ύ Utkast lagret i databasen!") - - await browser.close() - - finally: - await conn.close() - print("\n🏁 Skraping fullfΓΈrt.") - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Skrap medlemskapspriser via AI.") - parser.add_argument("--ids", type=str, help="Kommaseparert liste med facility IDs (eks: 1,5,12)") - args = parser.parse_args() - - ids_to_scrape = None - if args.ids: - ids_to_scrape = [int(x.strip()) for x in args.ids.split(",")] - - asyncio.run(run_scraper(ids_to_scrape)) \ No newline at end of file diff --git a/kode_eksport_1/backend_scrape_nsg_3_py.txt b/kode_eksport_1/backend_scrape_nsg_3_py.txt deleted file mode 100644 index 190b12d..0000000 --- a/kode_eksport_1/backend_scrape_nsg_3_py.txt +++ /dev/null @@ -1,96 +0,0 @@ -import asyncio -import asyncpg -import httpx -from bs4 import BeautifulSoup -import re -import json - -DB_URL = "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff" - -def clean_name(text): - if not text: return "" - s = text.lower().replace("golfklubb", "").replace("gk", "").replace("par3golf", "").replace(" & ", "").strip() - return re.sub(r'[^a-z]', '', s) - -def clean_nsg_content(text): - """Fjerner doble linjeskift og kutter teksten fΓΈr websidemenyen starter""" - if not text: return "" - # Fjern alt som ligner pΓ₯ bunn-menyen til NSG - garbage_phrases = [ - "Klubbens hjemmeside", "Resultatlister i Golfbox", "Livescoring", - "Scoreinntasting", "Lagserie", "Turneringer", "Innmelding" - ] - for phrase in garbage_phrases: - text = text.split(phrase)[0] - - # Rydd opp i linjeskift og doble mellomrom - text = text.replace('\r', '').replace('\n', ' ') - text = re.sub(r'\s+', ' ', text).strip() - return text - -async def get_nsg_links(client): - links = [] - urls = ["https://seniorgolf.no/lojalitetskort-sitemap.xml", "https://seniorgolf.no/fordelskortet/"] - for url in urls: - try: - resp = await client.get(url) - if resp.status_code == 200: - if ".xml" in url: - found = re.findall(r'(https://seniorgolf.no/lojalitetskort/.*?/)', resp.text) - if found: return list(set(found)) - else: - soup = BeautifulSoup(resp.text, 'html.parser') - links.extend([l['href'] for l in soup.select('a[href*="/lojalitetskort/"]')]) - except: continue - return list(set(links)) - -async def scrape_nsg(): - print("πŸš€ Starter NSG VASKEMASKIN v3.8...") - conn = await asyncpg.connect(DB_URL) - facilities = await conn.fetch("SELECT id, name FROM facilities") - - async with httpx.AsyncClient(timeout=20.0, headers={'User-Agent': 'Mozilla/5.0'}) as client: - all_nsg_links = await get_nsg_links(client) - link_map = {clean_name(l.split('/')[-2].replace('-', ' ')): l for l in all_nsg_links} - - matches_found = 0 - for fac in facilities: - fac_name_clean = clean_name(fac['name']) - match_url = link_map.get(fac_name_clean) - - if not match_url: - for slug, url in link_map.items(): - if fac_name_clean in slug or slug in fac_name_clean: - match_url = url - break - - if match_url: - try: - f_resp = await client.get(match_url) - f_soup = BeautifulSoup(f_resp.text, 'html.parser') - - # Finn hovedinnholdet i stedet for hele siden for Γ₯ unngΓ₯ menyer - main_content = f_soup.find('div', {'class': 'entry-content'}) or f_soup - text = main_content.get_text() - - st = re.search(r"Starttider:?\s*(.*?)(?=Greenfee|Booking|Adresse|Kontakt|$)", text, re.S | re.I) - gf = re.search(r"Greenfee:?\s*(.*?)(?=Booking|Adresse|Kontakt|$)", text, re.S | re.I) - bk = re.search(r"Booking:?\s*(.*?)(?=Adresse|Kontakt|$)", text, re.S | re.I) - - nsg_data = { - "url": match_url, - "starttider": clean_nsg_content(st.group(1)) if st else "Se nettside", - "greenfee": clean_nsg_content(gf.group(1)) if gf else "Se nettside", - "booking": clean_nsg_content(bk.group(1)) if bk else "Se nettside" - } - - await conn.execute("UPDATE facilities SET nsg_data = $1 WHERE id = $2", json.dumps(nsg_data), fac['id']) - print(f"βœ… Vasket & Lagret: {fac['name']}") - matches_found += 1 - except: pass - - await conn.close() - print(f"\nπŸŽ‰ Vask ferdig! {matches_found} baner er nΓ₯ 100% klare.") - -if __name__ == "__main__": - asyncio.run(scrape_nsg()) diff --git a/kode_eksport_1/backend_scrape_status_py.txt b/kode_eksport_1/backend_scrape_status_py.txt deleted file mode 100644 index fb12838..0000000 --- a/kode_eksport_1/backend_scrape_status_py.txt +++ /dev/null @@ -1,332 +0,0 @@ -import asyncio -import os -import asyncpg -import smtplib -import re -import argparse -from datetime import datetime -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from playwright.async_api import async_playwright -try: - from playwright_stealth import stealth_async as apply_stealth -except ImportError: - from playwright_stealth import stealth as apply_stealth - -from google import genai -from dotenv import load_dotenv - -load_dotenv() - -DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") - -# ========================================== -# KONFIGURERER GEMINI AI (NY SDK) -# ========================================== -client = genai.Client() - -async def ask_llm_status(text, course_name, is_single_course, ai_instruction=None): - if is_single_course: - bane_instruks = "Finn den generelle banestatusen for dette golfanlegget. Se bort fra spesifikke banenavn, da anlegget kun har Γ©n bane." - else: - bane_instruks = f'Finn banestatusen SPESIFIKT for banen som heter/omtales som: "{course_name}".' - - ekstra_tekst = f"\n!!! VIKTIG EKSTRA-INSTRUKS FRA ADMIN (DENNE OVERSTYRER ALLE ANDRE REGLER) !!!:\n{ai_instruction}\n" if ai_instruction else "" - - prompt = f""" - Du er en ekspert pΓ₯ Γ₯ lese norske golfklubbers nettsider for Γ₯ finne banestatus. - {bane_instruks} - {ekstra_tekst} - Svar KUN med nΓΈyaktig ETT av disse ordene: - - aapen (hvis banen er Γ₯pen/sommergreener) - - stengt (hvis banen er lukket/stengt/frost/snΓΈ) - - aapen_med_vintergreener (hvis det spilles pΓ₯ vintergreener) - - aapner_snart (hvis den Γ₯pner om kort tid) - - stenger_snart (hvis den stenger for sesongen om kort tid) - - under_utvikling (hvis den er under utvikling) - - nedlagt (hvis den er nedlagt) - - ukjent (hvis du ikke finner noe info om banen i teksten) - - Tekst fra nettsiden: - {text[:15000]} - """ - - print("\n" + "="*60) - print(f"πŸ€– SENDER PROMPT TIL GEMINI FOR: '{course_name}'") - print(f"πŸ‘‰ STANDARD-INSTRUKS: {bane_instruks}") - if ai_instruction: - print(f"πŸ‘‰ ADMIN-HVISKER: {ai_instruction}") - clean_text_sample = " ".join(text.split())[:250] - print(f"πŸ‘‰ TEKST FRA NETTSIDEN (utdrag): '{clean_text_sample}...'") - print("="*60 + "\n") - - try: - response = await client.aio.models.generate_content( - model='gemini-2.5-flash', - contents=prompt - ) - svar = response.text.strip().lower() - - print(f" 🧠 GEMINI RΓ…-SVAR: '{svar}'") - - # --- NYTT: SORTERT SIKKERHETSFILTER --- - gyldige_svar = [ - "aapen_med_vintergreener", - "aapner_snart", - "stenger_snart", - "under_utvikling", - "nedlagt", - "stengt", - "aapen", - "ukjent" - ] - - for gyldig in gyldige_svar: - if gyldig in svar: - return gyldig - return "ukjent" - except Exception as e: - print(f"❌ Gemini Feil: {e}") - return "ukjent" - - -# ========================================== -# EKSISTERENDE LOGIKK FOR MANUELL SCRAPING -# ========================================== -def clean_text(text): - return re.sub(r'[^a-zA-Z0-9æøΓ₯Γ†Γ˜Γ…]', '', text).lower() - -def interpret_status(text, keyword=None): - t_raw = text.lower() - - if keyword: - k_clean = clean_text(keyword) - if k_clean not in clean_text(t_raw): - return "NOT_FOUND" - - parts = re.split(re.escape(keyword), t_raw, flags=re.IGNORECASE) - if len(parts) > 1: - t_raw = parts[1][:150] - else: - t_raw = t_raw[-200:] - - if any(word in t_raw for word in ["stengt", "lukket", "frost", "snΓΈ", "is", "closed", "stenger"]): - return "stengt" - if any(word in t_raw for word in ["vintergreen", "vintergrΓΈnn", "vinter"]): - return "aapen_med_vintergreener" - if any(word in t_raw for word in ["snart", "Γ₯pner kl"]): - return "aapner_snart" - if any(word in t_raw for word in ["Γ₯pen", "Γ₯pent", "aapen", "open"]): - return "aapen" - return "ukjent" - -def send_report(changes, warnings, successes): - if not changes and not warnings and not successes: return - subject = f"TeeOff Banestatus Rapport - {datetime.now().strftime('%d.%m.%Y')}" - - body = "BANESTATUS RAPPORT\n" + "="*30 + "\n\n" - - if changes: body += "βœ… OPPDATERINGER:\n" + "\n".join(changes) + "\n\n" - if warnings: body += "⚠️ MERKNADER / ADVARSLER:\n" + "\n".join(warnings) + "\n\n" - if successes: body += "πŸ†— VELLYKKEDE SJEKKER (INGEN ENDRING):\n" + "\n".join(successes) + "\n" - - msg = MIMEMultipart() - msg['From'] = os.getenv("SMTP_USER") - msg['To'] = os.getenv("EMAIL_TO") - msg['Subject'] = subject - msg.attach(MIMEText(body, 'plain')) - try: - with smtplib.SMTP_SSL(os.getenv("SMTP_SERVER"), int(os.getenv("SMTP_PORT"))) as server: - server.login(os.getenv("SMTP_USER"), os.getenv("SMTP_PASS")) - server.send_message(msg) - print("βœ… Rapport sendt pΓ₯ e-post.") - except Exception as e: - print(f"❌ E-post feil: {e}") - - -# ========================================== -# HOVEDMOTOR -# ========================================== -async def run_daily_scraping(facility_ids=None): - print(f"πŸš€ Starter sjekk {datetime.now().strftime('%H:%M:%S')}...") - conn = await asyncpg.connect(DB_URL) - - if facility_ids: - print(f"πŸ“Œ KjΓΈrer skraping KUN for anlegg-ID(er): {facility_ids}") - facilities = await conn.fetch( - "SELECT id, name, scrape_status_url, scrape_status_selector, scrape_method, ai_instruction FROM facilities WHERE scrape_status_url IS NOT NULL AND id = ANY($1::int[])", - facility_ids - ) - else: - print("🌍 KjΓΈrer skraping for ALLE anlegg med scrape_status_url...") - facilities = await conn.fetch( - "SELECT id, name, scrape_status_url, scrape_status_selector, scrape_method, ai_instruction FROM facilities WHERE scrape_status_url IS NOT NULL" - ) - - if not facilities: - print("⚠️ Fant ingen anlegg Γ₯ skrape.") - await conn.close() - return - - changes, warnings, successes = [], [], [] - - async with async_playwright() as p: - browser = await p.chromium.launch(headless=True) - context = await browser.new_context() - - for f in facilities: - method = f.get('scrape_method') or 'css_selector' - - if method == 'manual': - successes.append(f"⏸️ {f['name']}: Hoppet over (Manuell overstyring)") - print(f" ⏸️ Hopper over skraping av {f['name']} (Satt til Manuell)") - continue - - page = await context.new_page() - try: await apply_stealth(page) - except: pass - - try: - print(f"πŸ” BesΓΈker {f['name']} (Metode: {method})...") - await page.goto(f['scrape_status_url'], timeout=60000, wait_until="domcontentloaded") - await page.wait_for_timeout(3000) - - full_text = "" - - if method == 'css_selector': - element = page.locator(f['scrape_status_selector']).first - if await element.count() == 0: - warnings.append(f"❌ {f['name']}: Fant ikke CSS-elementet '{f['scrape_status_selector']}'") - continue - full_text = await element.inner_text() - - elif method == 'iframe_golfbox': - frame = page.frame_locator('iframe[src*="golfbox"]') - element = frame.locator(f['scrape_status_selector']).first - if await element.count() == 0: - warnings.append(f"❌ {f['name']}: Fant ikke elementet '{f['scrape_status_selector']}' i iframen") - continue - full_text = await element.inner_text() - - elif method == 'click_then_css': - parts = f['scrape_status_selector'].split('||') - if len(parts) != 2: - warnings.append(f"❌ {f['name']}: Ugyldig selector for click_then_css (mangler ||)") - continue - - btn_selector, text_selector = parts - btn = page.locator(btn_selector).first - if await btn.count() == 0: - warnings.append(f"❌ {f['name']}: Fant ikke knappen Γ₯ klikke pΓ₯: '{btn_selector}'") - continue - - await btn.click(force=True) - await page.wait_for_timeout(2000) - - element = page.locator(text_selector).first - if await element.count() == 0: - warnings.append(f"❌ {f['name']}: Fant ikke tekstboksen '{text_selector}' etter klikk") - continue - - full_text = await element.inner_text() - - elif method == 'llm_parse': - print(" πŸ–±οΈ Leter etter knapper Γ₯ klikke pΓ₯ for Γ₯ avdekke skjult tekst...") - knapper = await page.get_by_text(re.compile(r"banestatus|dagens status|se status|se dagens status|baneinfo|\bstatus\b", re.IGNORECASE)).all() - - klikk_count = 0 - for knapp in knapper: - try: - if await knapp.is_visible(): - await knapp.click(timeout=2000, force=True) - klikk_count += 1 - await page.wait_for_timeout(2000) - except Exception: - pass - - if klikk_count > 0: - print(f" 🎯 Tvangsklikket pΓ₯ {klikk_count} status-knapp(er)! Venter ekstra pΓ₯ at innholdet laster...") - await page.wait_for_timeout(2000) - else: - print(" ⚠️ Fant ingen knapper Γ₯ klikke pΓ₯.") - - # --- NYTT: HENTER OGSΓ… SKJULT TEKST (For Scangolf megamenyer) --- - element = page.locator("body").first - if await element.count() == 0: - warnings.append(f"❌ {f['name']}: Klarte ikke Γ₯ lese siden for AI-tolkning") - continue - - synlig_tekst = await element.inner_text() or "" - skjult_tekst = await element.text_content() or "" - - # SlΓ₯r sammen all tekst slik at Gemini fΓ₯r med seg menyer som er gjemt med CSS - rΓ₯tekst = synlig_tekst + " " + skjult_tekst - full_text = " ".join(rΓ₯tekst.split()) - # ---------------------------------------------------------------- - - else: - warnings.append(f"⚠️ {f['name']}: Ukjent skrapemetode i databasen: '{method}'") - continue - - await conn.execute("UPDATE facilities SET status_updated_at = CURRENT_DATE WHERE id = $1", f['id']) - - courses = await conn.fetch("SELECT id, name, status, scrape_keyword FROM courses WHERE facility_id = $1", f['id']) - - is_single_course = len(courses) == 1 - - for c in courses: - old_status = c['status'] or "ukjent" - - if method == 'llm_parse': - print(f" πŸ€– SpΓΈr Gemini om status for '{c['name']}' (Singelbane: {is_single_course})...") - new_status = await ask_llm_status(full_text, c['name'], is_single_course, f.get('ai_instruction')) - - print(" ⏳ Tar 5 sekunders pause for Γ₯ spare Gemini-kvoten...") - await asyncio.sleep(5) - else: - new_status = interpret_status(full_text, c['scrape_keyword']) - - if new_status == "NOT_FOUND": - warnings.append(f"❓ {f['name']} ({c['name']}): Fant ikke sΓΈkeordet '{c['scrape_keyword']}' i teksten.") - continue - - # --- OPPDATERT LOGIKK (Fikser logg-buggen) --- - if new_status == "ukjent": - # Sikkerhetsnettet slΓ₯r inn: Vi beholder gammel status! - warnings.append(f"⚠️ {f['name']} ({c['name']}): Fant ikke status. Beholder '{old_status.upper()}'.") - print(f" 🟑 KONKLUSJON: Fant ikke status i teksten (Sikkerhetsnett). Beholder gammel status ({old_status.upper()}).") - elif new_status != old_status: - await conn.execute("UPDATE courses SET status = $1 WHERE id = $2", new_status, c['id']) - changes.append(f"πŸ”Ή {f['name']} ({c['name']}): {old_status.upper()} βž” {new_status.upper()}") - print(f" 🟒 KONKLUSJON: Status endret fra {old_status.upper()} til {new_status.upper()}") - else: - successes.append(f"βœ… {f['name']} ({c['name']}): {new_status.upper()}") - print(f" βšͺ KONKLUSJON: Ingen endring. Banen er fortsatt {old_status.upper()}") - # --------------------------------------------- - - except Exception as e: - err_msg = str(e).split('\n')[0] - warnings.append(f"πŸ”₯ {f['name']}: Feil under skraping: {err_msg}") - finally: - await page.close() - - await browser.close() - - await conn.close() - send_report(changes, warnings, successes) - print("🏁 Ferdig.") - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="TeeOff Status Scraper") - parser.add_argument("--ids", type=str, help="Kommaseparert liste med anleggs-IDer", default=None) - args = parser.parse_args() - - facility_ids_list = None - if args.ids: - try: - facility_ids_list = [int(id_str.strip()) for id_str in args.ids.split(",") if id_str.strip()] - except ValueError: - print("❌ Feil format pΓ₯ --ids. MΓ₯ vΓ¦re kommaseparerte tall, f.eks: 1,4,12") - exit(1) - - asyncio.run(run_daily_scraping(facility_ids_list)) \ No newline at end of file diff --git a/kode_eksport_1/backend_scrape_vtg_py.txt b/kode_eksport_1/backend_scrape_vtg_py.txt deleted file mode 100644 index 797545d..0000000 --- a/kode_eksport_1/backend_scrape_vtg_py.txt +++ /dev/null @@ -1,161 +0,0 @@ -""" -TEE OFF - VEIEN TIL GOLF (VTG) SKRAPER MED GEMINI AI ---------------------------------------------------------------------------- -Henter pris, beskrivelse (inkl. lΓ₯nekΓΈller/medlemskap) og kursdatoer fra VTG-sider. -StΓΈtter kommaseparerte URL-er. ---------------------------------------------------------------------------- -""" - -import asyncio -import asyncpg -import os -import json -import argparse -from bs4 import BeautifulSoup -from playwright.async_api import async_playwright -import google.generativeai as genai -from dotenv import load_dotenv - -load_dotenv() - -DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") -GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") - -if not GEMINI_API_KEY: - raise ValueError("🚨 GEMINI_API_KEY mangler i .env filen!") - -genai.configure(api_key=GEMINI_API_KEY) -model = genai.GenerativeModel('gemini-2.5-flash') - -async def fetch_page_text(url: str, browser) -> str: - url = url.strip() - if not url.startswith("http"): - return "" - - print(f" 🌐 Laster inn: {url}") - try: - page = await browser.new_page() - await page.goto(url, wait_until="domcontentloaded", timeout=15000) - html_content = await page.content() - await page.close() - - soup = BeautifulSoup(html_content, 'html.parser') - for script in soup(["script", "style", "nav", "footer", "header"]): - script.extract() - - return soup.get_text(separator=' ', strip=True) - except Exception as e: - print(f" ❌ Feil ved lasting av {url}: {e}") - return "" - -def analyze_vtg_with_gemini(text: str, club_name: str) -> dict: - print(f" 🧠 Sender {len(text)} tegn til Gemini for VTG-analyse...") - - prompt = f""" -Du er en ekspert pΓ₯ norske golfklubber. Din oppgave er Γ₯ lese en lang tekst fra nettsidene til "{club_name}" og koke dette ned til essensen om deres "Veien til Golf" (VTG) nybegynnerkurs. - -OPPGAVER: -1. Finn standardprisen for VTG-kurset for en vanlig voksen person. (Returner KUN tallet). -2. Skriv en KOMPRIMERT, selgende beskrivelse (maks 3-4 setninger). Du MΓ… inkludere informasjon om: - - Er lΓ₯n av kΓΈller/utstyr inkludert i kurset? - - Inkluderer prisen et medlemskap/spillerett i klubben (og ev. for hvor lenge)? - - Hva er omfanget? (F.eks. "12 timer praksis pluss e-lΓ¦ring"). - Ignorer uvesentlig stΓΈy og lange historiske utgreiinger. -3. Finn alle kommende kursdatoer. Finn startdato/sluttdato for hvert kurs, og noter status ("Ledig", "Fulltegnet", "Venteliste"). - -TEKST FRA NETTSIDEN: -{text} - -OPPGAVE: -Returner KUN et gyldig JSON-objekt med nΓΈyaktig fΓΈlgende struktur: -{{ - "foreslatt_vtg_pris": 1990, - "foreslatt_vtg_beskrivelse": "Kurset gΓ₯r over 12 timer inkludert obligatorisk e-lΓ¦ring. LΓ₯n av golfkΓΈller er inkludert under hele kurset, og prisen gir deg ogsΓ₯ fritt spill og medlemskap ut Γ₯ret.", - "foreslatt_vtg_datoer": [ - {{"dato": "12.-14. mai", "status": "Fulltegnet"}}, - {{"dato": "5.-7. juni", "status": "Ledig"}} - ], - "ai_begrunnelse": "Fant voksenpris pΓ₯ 1990,-. Teksten nevnte eksplisitt at medlemskap ut Γ₯ret er med i prisen, og at man fΓ₯r lΓ₯ne utstyr." -}} -Merk: Sett foreslatt_vtg_pris til null (null) hvis du ikke finner den. Hvis du ikke finner datoer, la listen vΓ¦re tom []. -""" - - try: - response = model.generate_content(prompt) - raw_response = response.text.strip() - - if raw_response.startswith("```json"): - raw_response = raw_response[7:] - if raw_response.endswith("```"): - raw_response = raw_response[:-3] - - return json.loads(raw_response.strip()) - except Exception as e: - print(f" ❌ AI-analyse feilet: {e}") - return None - -async def run_vtg_scraper(facility_ids=None): - print("πŸš€ Starter Veien til Golf (VTG) skraperen...") - conn = await asyncpg.connect(DB_URL) - - try: - query = "SELECT id, name, vtg_lenke FROM facilities WHERE vtg_lenke IS NOT NULL AND vtg_lenke != ''" - if facility_ids: - query += f" AND id IN ({','.join(map(str, facility_ids))})" - - facilities = await conn.fetch(query) - print(f"πŸ“‹ Fant {len(facilities)} anlegg Γ₯ skrape.") - - async with async_playwright() as p: - browser = await p.chromium.launch(headless=True) - - for facility in facilities: - fac_id = facility['id'] - name = facility['name'] - urls_raw = facility['vtg_lenke'] - - print(f"\n▢️ Behandler VTG for: {name} (ID: {fac_id})") - - urls = [u.strip() for u in urls_raw.split(',')] - combined_text = "" - - for idx, url in enumerate(urls, 1): - page_text = await fetch_page_text(url, browser) - if page_text: - combined_text += f"\n\n--- TEKST FRA SIDE {idx} ({url}) ---\n{page_text}" - - if len(combined_text) < 50: - print(" ⚠️ Fant for lite tekst, hopper over.") - continue - - draft_data = analyze_vtg_with_gemini(combined_text[:25000], name) - - if not draft_data: - continue - - print(f" βœ… AI fant pris: {draft_data.get('foreslatt_vtg_pris')}, og {len(draft_data.get('foreslatt_vtg_datoer', []))} datoer.") - - await conn.execute(""" - UPDATE facilities - SET vtg_draft = $1::jsonb - WHERE id = $2 - """, json.dumps(draft_data), fac_id) - - print(" πŸ’Ύ VTG-utkast lagret i databasen!") - - await browser.close() - - finally: - await conn.close() - print("\n🏁 Skraping fullfΓΈrt.") - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Skrap VTG via AI.") - parser.add_argument("--ids", type=str, help="Kommaseparert liste med facility IDs (eks: 1,5,12)") - args = parser.parse_args() - - ids_to_scrape = None - if args.ids: - ids_to_scrape = [int(x.strip()) for x in args.ids.split(",")] - - asyncio.run(run_vtg_scraper(ids_to_scrape)) \ No newline at end of file diff --git a/kode_eksport_1/backend_sync_greenfee_py.txt b/kode_eksport_1/backend_sync_greenfee_py.txt deleted file mode 100644 index 745a80e..0000000 --- a/kode_eksport_1/backend_sync_greenfee_py.txt +++ /dev/null @@ -1,79 +0,0 @@ -import asyncio, asyncpg, urllib.request, json - -DB_URL = "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff" -# Vi fjerner acf_format=standard da rΓ₯-feltnavnene er tryggere her -WP_API_URL = "https://teeoff.no/wp-json/wp/v2/golfbaner?per_page=100" - -def decode_html(text): - if not text: return "" - return str(text).replace('&', '&').replace('&', '&').replace(' ', ' ').strip() - -async def run_greenfee_sync(): - print("🎯 Starter GREENFEE-SYNC v1.2 (Basert pΓ₯ rΓ₯-API mapping)...") - conn = await asyncpg.connect(DB_URL) - page = 1 - total_updated = 0 - - while True: - try: - req = urllib.request.Request(f"{WP_API_URL}&page={page}", headers={'User-Agent': 'TeeOff-Sync'}) - with urllib.request.urlopen(req) as response: - data = json.loads(response.read().decode()) - except: break - if not data: break - - for post in data: - slug = post['slug'] - acf = post.get('acf', {}) - - # Henter banenavn for Γ₯ gruppere riktig - bane_1_navn = acf.get('navn_pa_hovedbane') or "Hovedbanen" - bane_2_navn = acf.get('navn_pa_sekundar_bane') or "Bane 2" - - final_greenfee = [] - - # --- MAPPER BANE 1 (Voksne + Junior) --- - voksne_1 = acf.get('greenfee_-_voksne') or [] - junior_1 = acf.get('greenfee_-_junior') or [] - - for i, item in enumerate(voksne_1): - row = { - "banenavn": bane_1_navn, - "priskategori": item.get('priskategori'), - "pris_voksne": item.get('pris_voksne') - } - # Legger til juniorpris hvis den finnes pΓ₯ samme index - if i < len(junior_1): - row["pris_junior"] = junior_1[i].get('pris_junior') - final_greenfee.append(row) - - # --- MAPPER BANE 2 (Voksne + Junior) --- - voksne_2 = acf.get('greenfee_-_voksne_bane_to') or [] - junior_2 = acf.get('greenfee_-_junior_bane_to') or [] - - for i, item in enumerate(voksne_2): - row = { - "banenavn": bane_2_navn, - "priskategori": item.get('priskategori_bane_to'), - "pris_voksne": item.get('pris_voksne_bane_to') - } - if i < len(junior_2): - row["pris_junior"] = junior_2[i].get('pris_junior_bane_to') - final_greenfee.append(row) - - # Henter krav (Gjeste_krav) - reqs = decode_html(acf.get('krav_til_gjestespillere')) - - if final_greenfee: - await conn.execute(''' - UPDATE facilities SET greenfee = $1::jsonb, guest_requirements = $2 WHERE slug = $3 - ''', json.dumps(final_greenfee), reqs, slug) - print(f"βœ… {slug}: Importerte {len(final_greenfee)} prisrader for {bane_1_navn}/{bane_2_navn}") - total_updated += 1 - - page += 1 - await conn.close() - print(f"\n✨ Ferdig! Oppdaterte priser for {total_updated} anlegg.") - -if __name__ == "__main__": - asyncio.run(run_greenfee_sync()) \ No newline at end of file diff --git a/kode_eksport_1/backend_test_gemini_py.txt b/kode_eksport_1/backend_test_gemini_py.txt deleted file mode 100644 index 1141ed3..0000000 --- a/kode_eksport_1/backend_test_gemini_py.txt +++ /dev/null @@ -1,116 +0,0 @@ -import asyncio -import os -import re -from playwright.async_api import async_playwright -from google import genai -from dotenv import load_dotenv - -load_dotenv() - -# Den nye pakken henter automatisk GEMINI_API_KEY fra .env-filen din -client = genai.Client() - -async def ask_llm_status(text, course_name, is_single_course): - if is_single_course: - bane_instruks = "Finn den generelle banestatusen for dette golfanlegget. Se bort fra spesifikke banenavn, da anlegget kun har Γ©n bane." - else: - bane_instruks = f'Finn banestatusen SPESIFIKT for banen som heter/omtales som: "{course_name}".' - - prompt = f""" - Du er en ekspert pΓ₯ Γ₯ lese norske golfklubbers nettsider for Γ₯ finne banestatus. - {bane_instruks} - Svar KUN med nΓΈyaktig ETT av disse ordene: - - aapen (hvis banen er Γ₯pen/sommergreener) - - stengt (hvis banen er lukket/stengt/frost/snΓΈ) - - aapen_med_vintergreener (hvis det spilles pΓ₯ vintergreener) - - aapner_snart (hvis den Γ₯pner om kort tid) - - stenger_snart (hvis den stenger for sesongen om kort tid) - - under_utvikling (hvis den er under utvikling) - - nedlagt (hvis den er nedlagt) - - ukjent (hvis du ikke finner noe info om banen i teksten) - - Tekst fra nettsiden: - {text[:15000]} - """ - - try: - # Ny mΓ₯te Γ₯ kalle modellen asynkront pΓ₯ med google-genai - response = await client.aio.models.generate_content( - model='gemini-2.5-flash', - contents=prompt - ) - svar = response.text.strip().lower() - - gyldige_svar = [ - "aapen", "stengt", "aapen_med_vintergreener", - "aapner_snart", "stenger_snart", "under_utvikling", - "nedlagt", "ukjent" - ] - - for gyldig in gyldige_svar: - if gyldig in svar: - return gyldig - return "ukjent" - except Exception as e: - print(f"❌ Gemini Feil: {e}") - return "ukjent" - -async def run_test(): - print("\n" + "="*50) - print(" πŸ§ͺ TEE OFF: GEMINI TEST-VERKTØY (MED AUTO-KLIKKER)") - print("="*50) - - url = input("🌐 Skriv inn URL til golfklubben (f.eks. https://oslogk.no): ").strip() - if not url.startswith("http"): - url = "https://" + url - - course_name = input("β›³ Skriv inn banenavn (eller trykk ENTER hvis anlegget kun har 1 bane): ").strip() - is_single = len(course_name) == 0 - - print("\n⏳ 1. Starter nettleser og besΓΈker siden...") - - full_text = "" - async with async_playwright() as p: - browser = await p.chromium.launch(headless=True) - page = await browser.new_page() - try: - await page.goto(url, timeout=30000, wait_until="domcontentloaded") - await asyncio.sleep(3) # Vent pΓ₯ animasjoner og iframes - - # --- NY LOGIKK: AUTO-KLIKKER --- - print("πŸ–±οΈ Leter etter 'banestatus'-knapper Γ₯ klikke pΓ₯...") - # Vi leter etter tekst som inneholder "banestatus" (ignorerer store/smΓ₯ bokstaver) - knapper = await page.get_by_text(re.compile(r"banestatus", re.IGNORECASE)).all() - - for knapp in knapper: - try: - if await knapp.is_visible(): - await knapp.click(timeout=3000) - print(" 🎯 Klikket pΓ₯ en banestatus-knapp! Venter 2 sekunder...") - await asyncio.sleep(2) # Venter pΓ₯ at modalen/pop-upen Γ₯pner seg - break # Vi trenger bare Γ₯ klikke pΓ₯ den fΓΈrste vi finner - except Exception as e: - # Ignorerer hvis knappen ikke er klikkbar, prΓΈver neste - pass - # -------------------------------- - - element = page.locator("body").first - rΓ₯tekst = await element.inner_text() - full_text = " ".join(rΓ₯tekst.split()) - print(f"βœ… Hentet {len(full_text)} tegn med tekst fra nettsiden.") - - except Exception as e: - print(f"❌ Feil ved innlasting av side: {e}") - await browser.close() - return - await browser.close() - - print("🧠 2. Sender teksten til Gemini for analyse...") - status = await ask_llm_status(full_text, course_name, is_single) - - print("\n" + "="*50) - print(f"🎯 GEMINI SITT SVAR: {status.upper()}") - print("="*50 + "\n") - -if __name__ == "__main__": - asyncio.run(run_test()) \ No newline at end of file diff --git a/kode_eksport_1/backend_test_login_py.txt b/kode_eksport_1/backend_test_login_py.txt deleted file mode 100644 index f026cbf..0000000 --- a/kode_eksport_1/backend_test_login_py.txt +++ /dev/null @@ -1,47 +0,0 @@ -import asyncio -import asyncpg -import os -from passlib.context import CryptContext - -DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") - -# Vi setter opp passord-sjekkeren AKKURAT slik main.py gjΓΈr det -pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto") - -async def test_sannheten(): - print("\n" + "="*50) - print(" πŸ” TEE OFF SANNHETSSERUM") - print("="*50) - - username = "Envide Webutvikling" - test_password = "Solveig Vilde Ingvild Gina" # SΓΈrg for at dette er det du satte sist! - - try: - conn = await asyncpg.connect(DB_URL) - row = await conn.fetchrow("SELECT password_hash FROM admins WHERE username = $1", username) - - if not row: - print("❌ FEIL: Fant ikke brukeren i det hele tatt!") - return - - db_hash = row['password_hash'] - print(f"1. Hash funnet i databasen: {db_hash[:30]}...") - - print(f"2. Tester mot passordet: '{test_password}'") - - # Den magiske testen - is_valid = pwd_context.verify(test_password, db_hash) - - print("-" * 50) - if is_valid: - print("βœ… SUKSESS! Passordet og hashen stemmer 100% overens.") - print("➑️ KONKLUSJON: Hashingen fungerer perfekt. Problemet MΓ… vΓ¦re at FastAPI (main.py) ikke klarer Γ₯ lese JSON-dataene fra curl/frontend riktig.") - else: - print("❌ FEIL! Passordet stemmer IKKE med hashen i databasen.") - print("➑️ KONKLUSJON: Scriptet som oppdaterer passordet gjΓΈr en feil (f.eks. legger til usynlige tegn), eller lagringen i databasen blir korrupt.") - - finally: - await conn.close() - -if __name__ == "__main__": - asyncio.run(test_sannheten()) \ No newline at end of file diff --git a/kode_eksport_1/backend_update_admin_py.txt b/kode_eksport_1/backend_update_admin_py.txt deleted file mode 100644 index 4883f01..0000000 --- a/kode_eksport_1/backend_update_admin_py.txt +++ /dev/null @@ -1,85 +0,0 @@ -""" -TEE OFF ADMIN PASSWORD UPDATER (API CONTAINER VERSION) ---------------------------------------------------------------------------- -FUNKSJON: Kobler direkte til databasen inni API-containeren, sjekker at - brukeren finnes, og utfΓΈrer passordoppdateringen automatisk. -STATUS: PΓ₯virker IKKE tofaktor (2FA). GjΓΈr jobben fra start til slutt. ---------------------------------------------------------------------------- -""" -import asyncio -import asyncpg -import os -import sys -import getpass -from passlib.hash import pbkdf2_sha256 - -# Henter database-URL fra miljΓΈvariabler (samme metode som backenden din bruker) -DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") - -async def update_admin_password(): - print("\n" + "="*50) - print(" TEE OFF ADMIN PASSORD-OPPDATERER (DIREKTE TILKOBLING)") - print("="*50) - - # Kobler til databasen pΓ₯ ekte backend-vis - try: - conn = await asyncpg.connect(DB_URL) - except Exception as e: - print(f"❌ Kunne ikke koble til databasen: {e}") - sys.exit(1) - - try: - # Brukernavn-verifisering - while True: - username = input("Brukernavn pΓ₯ admin som skal oppdateres: ").strip() - - print("⏳ Sjekker databasen...") - # SpΓΈr databasen direkte hvor mange som har dette navnet - count = await conn.fetchval("SELECT COUNT(*) FROM admins WHERE username = $1", username) - - if count == 0: - print(f"❌ Fant ingen bruker med navnet '{username}'. PrΓΈv igjen.\n") - elif count > 1: - print(f"⚠️ KRITISK FEIL: Fant {count} brukere med navnet '{username}'. Avbryter.") - sys.exit(1) - else: - print(f"βœ… Bruker '{username}' funnet i databasen!\n") - break - - # Passord-verifisering - while True: - password = getpass.getpass("Skriv inn NYTT passord: ") - password_confirm = getpass.getpass("Gjenta NYTT passord: ") - - if password == password_confirm: - if len(password) < 8: - print("⚠️ Advarsel: Passordet bΓΈr vΓ¦re minst 8 tegn.") - print(f"\n[DEBUG] Passord akseptert.") - break - else: - print("❌ Passordene er ikke like. PrΓΈv igjen.\n") - - print("⏳ Genererer PBKDF2-hash...") - password_hash = pbkdf2_sha256.hash(password) - - print("⏳ Oppdaterer databasen automatisk...") - # UtfΓΈrer selve oppdateringen (sikret mot SQL-injeksjoner) - await conn.execute("UPDATE admins SET password_hash = $1 WHERE username = $2", password_hash, username) - - print("\nβœ… PASSORD OPPDATERT VELLYKKET!") - print("-" * 50) - print(f"Passordet for '{username}' er nΓ₯ endret i databasen.") - print("Tofaktor (2FA) og alt annet er beholdt urΓΈrt.") - print("-" * 50 + "\n") - - finally: - # Lukk tilkoblingen pent - await conn.close() - -if __name__ == "__main__": - try: - # Siden vi bruker asyncpg, mΓ₯ scriptet kjΓΈres i en asyncio-loop - asyncio.run(update_admin_password()) - except KeyboardInterrupt: - print("\nAvbrutt.") - sys.exit(0) \ No newline at end of file diff --git a/kode_eksport_1/eksport_script_py.txt b/kode_eksport_1/eksport_script_py.txt deleted file mode 100644 index 288a706..0000000 --- a/kode_eksport_1/eksport_script_py.txt +++ /dev/null @@ -1,72 +0,0 @@ -import os -import shutil -from pathlib import Path - -# --- KONFIGURASJON --- -KILDE_MAPPE = "/opt/teeoff/" -EKSPORT_MAPPE = "/opt/teeoff/kode_eksport_1/" -TRE_FIL = "/opt/teeoff/fil-tre.txt" - -# Filtyper vi vil kopiere -FILTYPER = ['.py', '.ts', '.tsx'] - -# Mapper vi IKKE vil ha med i treet eller skanne (sparer tid og rot) -IGNORER_MAPPER = ['.git', 'node_modules', '__pycache__', 'kode_eksport', '.next'] - -def generer_tre_og_kopier(): - kilde_sti = Path(KILDE_MAPPE) - eksport_sti = Path(EKSPORT_MAPPE) - - # 1. Opprett eksportmappen hvis den ikke finnes - eksport_sti.mkdir(parents=True, exist_ok=True) - - tre_linjer = [] - kopierte_filer = 0 - - print("Skanner filer og genererer tre...") - - # 2. GΓ₯ gjennom alle mapper og filer - for root, dirs, files in os.walk(kilde_sti): - # Fjern ignorerte mapper sΓ₯ vi ikke gΓ₯r inn i dem - dirs[:] = [d for d in dirs if d not in IGNORER_MAPPER] - - # Regn ut innrykk basert pΓ₯ hvor dypt vi er i mappestrukturen - nivaa = root.replace(KILDE_MAPPE, '').count(os.sep) - innrykk = ' ' * 4 * nivaa - mappe_navn = os.path.basename(root) - - # Legg til mappen i treet - if mappe_navn: - tre_linjer.append(f"{innrykk}πŸ“ {mappe_navn}/") - else: - tre_linjer.append(f"πŸ“ {kilde_sti.name}/") - - sub_innrykk = ' ' * 4 * (nivaa + 1) - - # 3. GΓ₯ gjennom filene i mappen - for fil in files: - tre_linjer.append(f"{sub_innrykk}πŸ“„ {fil}") - - fil_sti = Path(root) / fil - - # 4. Sjekk om filen har riktig endelse og skal kopieres - if fil_sti.suffix in FILTYPER: - # Lag et unikt filnavn for Γ₯ unngΓ₯ overskriving - relativ_sti = fil_sti.relative_to(kilde_sti) - nytt_navn = str(relativ_sti).replace(os.sep, '_').replace('.', '_') + '.txt' - ny_sti = eksport_sti / nytt_navn - - # Kopier filen - shutil.copy2(fil_sti, ny_sti) - kopierte_filer += 1 - - # 5. Lagre filteret til tekstfilen - with open(TRE_FIL, 'w', encoding='utf-8') as f: - f.write('\n'.join(tre_linjer)) - - print(f"\nβœ… Ferdig!") - print(f"πŸ“ Filtre er lagret i: {TRE_FIL}") - print(f"πŸ“ Kopierte {kopierte_filer} kodefiler til: {EKSPORT_MAPPE}") - -if __name__ == "__main__": - generer_tre_og_kopier() diff --git a/kode_eksport_1/frontend_next-env_d_ts.txt b/kode_eksport_1/frontend_next-env_d_ts.txt deleted file mode 100644 index c4b7818..0000000 --- a/kode_eksport_1/frontend_next-env_d_ts.txt +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -import "./.next/dev/types/routes.d.ts"; - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/kode_eksport_1/frontend_next_config_ts.txt b/kode_eksport_1/frontend_next_config_ts.txt deleted file mode 100644 index e9ffa30..0000000 --- a/kode_eksport_1/frontend_next_config_ts.txt +++ /dev/null @@ -1,7 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - /* config options here */ -}; - -export default nextConfig; diff --git a/kode_eksport_1/frontend_src_app_FacilitySearch_tsx.txt b/kode_eksport_1/frontend_src_app_FacilitySearch_tsx.txt deleted file mode 100644 index ad1ce74..0000000 --- a/kode_eksport_1/frontend_src_app_FacilitySearch_tsx.txt +++ /dev/null @@ -1,217 +0,0 @@ -"use client"; -/** - * TEE OFF SYSTEM INSTRUCTIONS - FACILITY CARDS v3.8 (BLOB SEARCH) - * --------------------------------------------------------------------------- - * REGEL 1: Status-badge SKAL vises ΓΈverst til venstre FOR ALLE BANER. - * Bruk STATUS_MAP for tekst. - * REGEL 2: DATA-PARSING: Bruk parseJson() for 'course_statuses', 'amenities' og 'nsg_data'. - * REGEL 3: Avstand-pillen skal ha fargen #2d3319 (MΓΈrk oliven) med hvit tekst. - * REGEL 4: NSG (BlΓ₯ 'N') og Golfamore (Oransje 'G') sirkler skal ha hvit kant (border-2). - * REGEL 5: Bunnen: Antall Hull (grΓΈnn pill), Banetype (grΓ₯ pill), og Ikon-sirkler. - * REGEL 6: Viser dato (f.eks "05. mars 2026") rett til hΓΈyre for ΓΈverste status-pille. - * REGEL 7: Natural Language Search bruker en "Search Blob" for Γ₯ stΓΈtte delvise - * ord og skrivefeil slik at listen ikke tΓΈmmes mens brukeren skriver. - * --------------------------------------------------------------------------- - */ - -import { STATUS_MAP, REGIONS } from "@/config/constants"; -import { useState, useEffect, useMemo } from 'react'; -import Link from 'next/link'; - -function getDistance(lat1: number, lon1: number, lat2: number, lon2: number) { - try { - const R = 6371; - const dLat = (lat2 - lat1) * Math.PI / 180; - const dLon = (lon2 - lon1) * Math.PI / 180; - const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); - return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - } catch (e) { return Infinity; } -} - -export default function FacilitySearch({ initialFacilities }: { initialFacilities: any[] }) { - const [searchQuery, setSearchQuery] = useState(""); - const [userLocation, setUserLocation] = useState<{ lat: number, lng: number } | null>(null); - const [sortMethod, setSortMethod] = useState<'dist' | 'alpha'>('alpha'); - - useEffect(() => { - if ("geolocation" in navigator) { - navigator.geolocation.getCurrentPosition(p => { - setUserLocation({ lat: p.coords.latitude, lng: p.coords.longitude }); - setSortMethod('dist'); - }); - } - }, []); - - const processed = useMemo(() => { - if (!Array.isArray(initialFacilities)) return []; - - // Fyllord som fjernes slik at "Γ…pne baner i Oslo" blir til sΓΈkeordene ["Γ₯pne", "oslo"] - const stopWords = new Set(["i", "pΓ₯", "for", "med", "av", "og"]); - - return initialFacilities.map(f => { - // --- ROBUST DATA-PARSING --- - const parseJson = (val: any, fallback: any) => { - if (!val) return fallback; - if (typeof val === 'object') return val; - try { return JSON.parse(val); } catch (e) { return fallback; } - }; - - const rawStatuses = parseJson(f.course_statuses, []); - const sArr = Array.isArray(rawStatuses) && rawStatuses.length > 0 - ? rawStatuses - : [{ status: 'ukjent', name: 'Hovedbane' }]; - - const amenities = parseJson(f.amenities, {}); - const nsgData = parseJson(f.nsg_data, {}); - - const dist = userLocation && f.lat && f.lng ? getDistance(userLocation.lat, userLocation.lng, f.lat, f.lng) : Infinity; - const hasNSG = nsgData && Object.keys(nsgData).length > 0; - const hasGolfamore = f.golfamore === true; - - // --- THE SEARCH BLOB --- - // Vi starter med Γ₯ legge navn, by og fylke i en stor, usynlig tekststreng - let searchableText = `${f.name} ${f.city} ${f.county}`.toLowerCase(); - - // 1. Injiser statuser i tekststrengen - const hasOpen = sArr.some((c: any) => (c.status || "") === 'aapen'); - const hasClosed = sArr.some((c: any) => (c.status || "") === 'stengt'); - const hasWinter = sArr.some((c: any) => (c.status || "") === 'aapen_med_vintergreener'); - const hasNedlagt = sArr.some((c: any) => (c.status || "") === 'nedlagt'); - - if (hasOpen) searchableText += " Γ₯pen Γ₯pne aapen"; - if (hasClosed) searchableText += " stengt stengte"; - if (hasWinter) searchableText += " vinter vintergreener vinterbane"; - if (hasNedlagt) searchableText += " nedlagt nedlagte"; - - // 2. Injiser spesial-tags - if (hasNSG) searchableText += " nsg norsk seniorgolf"; - if (hasGolfamore) searchableText += " golfamore amore"; - - // 3. Injiser landsdel (f.eks. hvis fylket er Akershus, legger vi til "ΓΈstlandet") - const fylke = (f.county || "").toLowerCase(); - Object.entries(REGIONS).forEach(([regionName, counties]) => { - if (counties.includes(fylke)) { - searchableText += ` ${regionName}`; - } - }); - - // Splitter brukerens sΓΈk inn i enkeltord og fjerner stopWords + ordene "bane"/"baner" - const words = searchQuery - .toLowerCase() - .trim() - .split(/\s+/) - .filter(w => w.length > 0 && !stopWords.has(w) && w !== "bane" && w !== "baner"); - - // Sjekker at ALLE ordene brukeren har skrevet, finnes et sted i "Search Blob"-en - const matches = words.every(w => searchableText.includes(w)); - - return { ...f, statuses: sArr, amenities, dist, hasNSG, hasGolfamore, matches }; - }) - .filter(f => f.matches) - .sort((a, b) => { - if (sortMethod === 'dist' && a.dist !== b.dist) return a.dist - b.dist; - return a.name.localeCompare(b.name, 'nb'); - }); - }, [searchQuery, initialFacilities, userLocation, sortMethod]); - - return ( -
-
- -
- - setSearchQuery(e.target.value)} /> - -
- {processed.map((f: any) => { - const sArr = f.statuses; // Sikret via pre-prosesseringen over - - // Formater datoen pent: "05. mars 2026" - const lastUpdated = f.status_updated_at - ? new Date(f.status_updated_at).toLocaleDateString('nb-NO', { day: '2-digit', month: 'long', year: 'numeric' }) - : 'Ukjent'; - - return ( - -
- {f.name} - - {/* Status Badges for ALLE baner pΓ₯ anlegget */} -
- {sArr.map((course: any, idx: number) => { - const rawStatus = (course.status || "ukjent").toLowerCase(); - - let statusColor = "bg-gray-400"; - if (rawStatus === 'aapen') statusColor = "bg-[#8bc34a]"; - else if (rawStatus.includes('vinter') || rawStatus === 'stenger_snart') statusColor = "bg-[#ff5722]"; - else if (rawStatus === 'aapner_snart') statusColor = "bg-amber-500"; - else if (rawStatus === 'stengt') statusColor = "bg-red-600"; - else if (rawStatus === 'nedlagt') statusColor = "bg-black"; - else if (rawStatus === 'under_utvikling') statusColor = "bg-blue-500"; - - return ( -
-
- {sArr.length > 1 && ( - - {course.name} - - )} - {STATUS_MAP[rawStatus] || rawStatus} -
- - {/* Dato-pille ved siden av den ΓΈverste status-pillen */} - {idx === 0 && ( -
- {lastUpdated} -
- )} -
- ); - })} -
- - {/* Avstandspille */} - {f.dist !== Infinity && ( -
- {Math.round(f.dist)} km unna -
- )} -
- -
-

{f.name}

-

{f.city} β€’ {f.county}

- -
-
- {/* Hull-pille */} - - {f.amenities?.antall_hull || '--'} HULL - - {/* Banetype-pille */} - - {f.banetype || 'SKOGSBANE'} - -
- - {/* Sirkel-ikoner (NSG / Golfamore) */} -
- {f.hasNSG && ( -
N
- )} - {f.hasGolfamore && ( -
G
- )} -
-
-
- - ); - })} -
-
- ); -} \ No newline at end of file diff --git a/kode_eksport_1/frontend_src_app_HeroSlider_tsx.txt b/kode_eksport_1/frontend_src_app_HeroSlider_tsx.txt deleted file mode 100644 index 515ce80..0000000 --- a/kode_eksport_1/frontend_src_app_HeroSlider_tsx.txt +++ /dev/null @@ -1,130 +0,0 @@ -"use client"; -/** - * TEE OFF SYSTEM INSTRUCTIONS - HERO SLIDER v2.4 - * --------------------------------------------------------------------------- - * REGEL 1: Kun baner med status 'aapen', 'aapner_snart', 'stenger_snart' - * eller 'aapen_med_vintergreener' skal prioriteres. - * REGEL 2: Baner med status 'nedlagt' eller 'under_utvikling' skal ALDRI vises. - * REGEL 3: Baner med generiske bilder (inneholder 'standard') skal ALDRI vises. - * REGEL 4: MANUELL EKSKLUDERING: Slugs i MANUAL_EXCLUSION_LIST skal aldri vises. - * REGEL 5: Slideren skal vise nΓΈyaktig 5 baner. - * REGEL 6: Maks hΓΈyde er lΓ₯st til 624px. Ingen badges. - * REGEL 7: Typografi: Nedjustert fontstΓΈrrelse (4xl mobil / 7xl desktop) for eleganse. - * REGEL 8: Utvalget skal vΓ¦re stabilt i Γ©n time (Hourly Seed) fΓΈr det refreshes. - * --------------------------------------------------------------------------- - */ - -import { useState, useEffect, useMemo } from 'react'; -import Link from 'next/link'; - -const MANUAL_EXCLUSION_LIST = [ - 'alsten-golfklubb', 'askim-golfklubb', 'bergen-golfklubb', 'eidskog-golfklubb', - 'eiker-golfklubb', 'floro-golfklubb', 'garder-golfklubb', 'hafjell-golfklubb', - 'halden-golfklubb', 'haugesund-golfklubb', 'hinnoy-golfklubb', 'hitra-golfklubb', - 'hurum-golfklubb', 'imjelt-pitch-putt', 'karmoy-golfklubb', 'kristiansund-og-omegn-golfklubb', - 'lommedalen-golfklubb', 'laerdal-golfklubb', 'moa-golfsenter', 'modum-golfklubb', - 'nes-golfklubb-09', 'nittedal-golfklubb', 'selbu-golfklubb', 'stryn-golfklubb', - 'sunnfjord-golfklubb', 'tysnes-golfklubb', 'vanylven-golfklubb', 'vesteralen-golfklubb', - 'vestlia-golf' -]; - -export default function HeroSlider({ facilities }: { facilities: any[] }) { - const [currentIndex, setCurrentSlide] = useState(0); - - const sliderItems = useMemo(() => { - if (!Array.isArray(facilities) || facilities.length === 0) return []; - - const preferredStatuses = ['aapen', 'aapner_snart', 'stenger_snart', 'aapen_med_vintergreener']; - const forbiddenStatuses = ['nedlagt', 'under_utvikling']; - - const validCandidates = facilities.filter(f => { - if (MANUAL_EXCLUSION_LIST.includes(f.slug)) return false; - const img = f.image_url || ""; - if (!img || img.toLowerCase().includes('standard') || img.length < 5) return false; - - const statuses = Array.isArray(f.course_statuses) ? f.course_statuses : []; - const isForbidden = statuses.some((s: any) => - forbiddenStatuses.includes((s.status || "").toLowerCase()) - ); - return !isForbidden; - }); - - const highPriority = validCandidates.filter(f => { - const statuses = Array.isArray(f.course_statuses) ? f.course_statuses : []; - return statuses.some((s: any) => preferredStatuses.includes((s.status || "").toLowerCase())); - }); - - const fallbackPool = validCandidates.filter(f => !highPriority.includes(f)); - const now = new Date(); - const hourlySeed = parseInt(`${now.getFullYear()}${now.getMonth()}${now.getDate()}${now.getHours()}`); - - const seededShuffle = (arr: any[]) => { - return [...arr].sort((a, b) => ((a.id * hourlySeed) % 100) - ((b.id * hourlySeed) % 100)); - }; - - let selection = seededShuffle(highPriority); - if (selection.length < 5) { - selection = [...selection, ...seededShuffle(fallbackPool)].slice(0, 5); - } else { - selection = selection.slice(0, 5); - } - return selection; - }, [facilities]); - - useEffect(() => { - if (sliderItems.length <= 1) return; - const interval = setInterval(() => setCurrentSlide((p) => (p + 1) % sliderItems.length), 8000); - return () => clearInterval(interval); - }, [sliderItems.length]); - - if (sliderItems.length === 0) return null; - - return ( -
- {sliderItems.map((f, i) => ( -
- -
- - {f.name} - -
-
-
- {/* FONT NEDJUSTERT FRA text-6xl md:text-9xl TIL text-4xl md:text-7xl */} -

- {f.name} -

-

- {f.county} β€’ {f.city} -

-
-
-
- -
- ))} - -
- {sliderItems.map((_, i) => ( -
-
- ); -} \ No newline at end of file diff --git a/kode_eksport_1/frontend_src_app_admin_greenfee_page_tsx.txt b/kode_eksport_1/frontend_src_app_admin_greenfee_page_tsx.txt deleted file mode 100644 index ed8a782..0000000 --- a/kode_eksport_1/frontend_src_app_admin_greenfee_page_tsx.txt +++ /dev/null @@ -1,203 +0,0 @@ -"use client"; -import { useState, useEffect } from 'react'; -import { API_URL } from "@/config/constants"; -import Link from 'next/link'; - -export default function GreenfeeWasher() { - const [drafts, setDrafts] = useState([]); - const [loading, setLoading] = useState(true); - const [selectedIds, setSelectedIds] = useState([]); - const [saving, setSaving] = useState(false); - - const fetchDrafts = () => { - setLoading(true); - fetch(`${API_URL}/admin/greenfee/drafts`) - .then(res => res.json()) - .then(data => { - const editableDrafts = data.map((f: any) => { - // JSONB fra Postgres kan noen ganger komme som en streng, - // vi mΓ₯ sikre at vi parser det hvis det trengs - let parsedDraft = f.greenfee_draft; - if (typeof parsedDraft === 'string') { - try { parsedDraft = JSON.parse(parsedDraft); } - catch (e) { console.error("Kunne ikke parse JSON", e); } - } - - // Hent ut selve listen (fallback til tom liste hvis noe er feil) - const greenfeeList = parsedDraft?.foreslatt_greenfee || []; - - return { - ...f, - greenfee_draft: parsedDraft, // Lagre den parsede versjonen for visning - edit_greenfee: greenfeeList // Dette er arrayet som binder seg til input-feltene - }; - }); - setDrafts(editableDrafts); - setLoading(false); - }) - .catch(() => setLoading(false)); - }; - - useEffect(() => { fetchDrafts(); }, []); - - const toggleSelectAll = (checked: boolean) => { - if (checked) setSelectedIds(drafts.map(d => d.id)); - else setSelectedIds([]); - }; - - const toggleOne = (id: number) => { - if (selectedIds.includes(id)) setSelectedIds(selectedIds.filter(i => i !== id)); - else setSelectedIds([...selectedIds, id]); - }; - - const removeRow = (facilityId: number, rowIndex: number) => { - setDrafts(drafts.map(d => { - if (d.id === facilityId) { - const newRows = [...d.edit_greenfee]; - newRows.splice(rowIndex, 1); - return { ...d, edit_greenfee: newRows }; - } - return d; - })); - }; - - const updateField = (facilityId: number, rowIndex: number, field: string, value: string | number) => { - setDrafts(drafts.map(d => { - if (d.id === facilityId) { - const newRows = [...d.edit_greenfee]; - newRows[rowIndex] = { ...newRows[rowIndex], [field]: value }; - return { ...d, edit_greenfee: newRows }; - } - return d; - })); - }; - - const handleApprove = async () => { - const toApprove = drafts.filter(d => selectedIds.includes(d.id)).map(d => ({ - facility_id: d.id, - greenfee: d.edit_greenfee.map((row: any) => ({ - banenavn: row.banenavn || "", - priskategori: row.priskategori || "", - pris_voksne: Number(row.pris_voksne) || null, - pris_junior: Number(row.pris_junior) || null - })) - })); - - if (toApprove.length === 0) return alert("Velg minst ett anlegg Γ₯ godkjenne."); - - setSaving(true); - try { - const res = await fetch(`${API_URL}/admin/greenfee/approve-bulk`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ approvals: toApprove }) - }); - if (res.ok) { - alert(`${toApprove.length} anlegg oppdatert!`); - setSelectedIds([]); - fetchDrafts(); - } else { - alert("Noe gikk galt under lagring."); - } - } catch (e) { - alert("Nettverksfeil"); - } - setSaving(false); - }; - - if (loading) return
Laster utkast...
; - - return ( -
-
-
-
- ← Tilbake til oversikten -

Greenfee-Vaskeriet

-

Sjekk at prisene gir mening fΓΈr publisering.

-
- -
- - {drafts.length === 0 ? ( -
- 🧹 -

Alt er rent og pent!

-
- ) : ( -
-
- 0} onChange={(e) => toggleSelectAll(e.target.checked)} /> - Velg Alle -
- - {drafts.map(draft => ( -
-
-
toggleOne(draft.id)} />
-
-
-

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

- Sjekk Nettside β†— -
- - {draft.greenfee_draft?.ai_begrunnelse && ( -
- πŸ€– AI Begrunnelse: {draft.greenfee_draft.ai_begrunnelse} -
- )} - - {draft.greenfee_draft?.foreslatt_avtaleklubber?.length > 0 && ( -
- 🀝 AI fant disse avtaleklubbene i teksten: {draft.greenfee_draft.foreslatt_avtaleklubber.join(', ')} -
- )} - -
-
-

Slik ser det ut i databasen nΓ₯:

-
- {draft.greenfee && draft.greenfee.length > 0 ? draft.greenfee.map((g: any, i: number) => ( -
- {g.banenavn} - {g.priskategori} - V: {g.pris_voksne || '-'} | J: {g.pris_junior || '-'} -
- )) : "Ingen priser registrert."} -
-
- -
-

Nytt forslag Γ₯ godkjenne:

-
- {draft.edit_greenfee && draft.edit_greenfee.map((row: any, idx: number) => ( -
- updateField(draft.id, idx, 'banenavn', e.target.value)} placeholder="Bane" /> - 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" /> - -
- ))} - -
-
-
-
-
-
- ))} -
- )} -
-
- ); -} \ No newline at end of file diff --git a/kode_eksport_1/frontend_src_app_admin_login_page_tsx.txt b/kode_eksport_1/frontend_src_app_admin_login_page_tsx.txt deleted file mode 100644 index 084efbd..0000000 --- a/kode_eksport_1/frontend_src_app_admin_login_page_tsx.txt +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; -/** - * TEE OFF ADMIN LOGIN v1.2 - * --------------------------------------------------------------------------- - * PLASSERING: frontend/src/app/admin/login/page.tsx - * FUNKSJON: Offentlig tilgjengelig innlogging for administratorer. - * --------------------------------------------------------------------------- - */ - -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { API_URL } from "@/config/constants"; - -export default function AdminLogin() { - const [step, setStep] = useState(1); - const [formData, setFormData] = useState({ username: '', password: '', code: '' }); - const [tempToken, setTempToken] = useState(''); - const [error, setError] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); - - const handleLogin = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(''); - - try { - const res = await fetch(`${API_URL}/auth/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username: formData.username, password: formData.password }) - }); - - const data = await res.json(); - if (res.ok) { - setTempToken(data.temp_token); - setStep(2); - } else { - setError(data.detail || 'Ugyldig pΓ₯logging'); - } - } catch (err) { - console.error("πŸ”₯ DEN EKTE FEILEN ER:", err); - setError('Systemfeil: Kunne ikke koble til API-et'); - } finally { - setIsLoading(false); - } - }; - - const handleVerify2FA = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - - try { - const res = await fetch(`${API_URL}/auth/verify-2fa`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ temp_token: tempToken, code: formData.code }) - }); - - if (res.ok) { - // VIKTIG: Etter suksess sender vi brukeren til selve dashbordet - router.push('/admin'); - router.refresh(); - } else { - setError('Ugyldig 2FA-kode'); - } - } catch (err) { - setError('Tilkoblingsfeil ved 2FA-verifisering'); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-
- TeeOff -
-

- {step === 1 ? "Admin Portalen" : "Tofaktor Sjekk"} -

-
- {step === 1 ? ( - <> - setFormData(prevState => ({...prevState, username: e.target.value}))} required /> - setFormData(prevState => ({...prevState, password: e.target.value}))} required /> - - ) : ( -
-

Tast inn 6 siffer fra appen din

- setFormData({...formData, code: e.target.value})} autoFocus required /> -
- )} - {error &&
⚠️ {error}
} - -
-
-
- ); -} \ No newline at end of file diff --git a/kode_eksport_1/frontend_src_app_admin_medlemskap_page_tsx.txt b/kode_eksport_1/frontend_src_app_admin_medlemskap_page_tsx.txt deleted file mode 100644 index 7c16052..0000000 --- a/kode_eksport_1/frontend_src_app_admin_medlemskap_page_tsx.txt +++ /dev/null @@ -1,179 +0,0 @@ -"use client"; -import { useState, useEffect } from 'react'; -import { API_URL } from "@/config/constants"; -import Link from 'next/link'; - -export default function MembershipWasher() { - const [drafts, setDrafts] = useState([]); - const [loading, setLoading] = useState(true); - const [selectedIds, setSelectedIds] = useState([]); - const [saving, setSaving] = useState(false); - - const fetchDrafts = () => { - setLoading(true); - fetch(`${API_URL}/admin/membership/drafts`) - .then(res => res.json()) - .then(data => { - // Konverter innkommende drafts til editerbare felter lokalt - const editableDrafts = data.map((f: any) => ({ - ...f, - edit_standard_navn: f.membership_draft?.foreslatt_standard_navn || f.navn_standard_medlemskap || "", - edit_standard_pris: f.membership_draft?.foreslatt_standard_pris || f.standard_medlemskap || "", - edit_standard_kommentar: f.membership_draft?.foreslatt_standard_kommentar || "", - edit_rimeligste_navn: f.membership_draft?.foreslatt_rimeligste_navn || f.navn_rimeligste_alternativ || "", - edit_rimeligste_pris: f.membership_draft?.foreslatt_rimeligste_pris || f.rimeligste_alternativ || "", - })); - setDrafts(editableDrafts); - setLoading(false); - }) - .catch(() => setLoading(false)); - }; - - useEffect(() => { - fetchDrafts(); - }, []); - - const toggleSelectAll = (checked: boolean) => { - if (checked) setSelectedIds(drafts.map(d => d.id)); - else setSelectedIds([]); - }; - - const toggleOne = (id: number) => { - if (selectedIds.includes(id)) setSelectedIds(selectedIds.filter(i => i !== id)); - else setSelectedIds([...selectedIds, id]); - }; - - const updateDraftField = (id: number, field: string, value: any) => { - setDrafts(drafts.map(d => d.id === id ? { ...d, [field]: value } : d)); - }; - - const handleApprove = async () => { - const toApprove = drafts.filter(d => selectedIds.includes(d.id)).map(d => ({ - facility_id: d.id, - navn_standard_medlemskap: d.edit_standard_navn, - standard_medlemskap: Number(d.edit_standard_pris) || null, - standard_medlemskap_kommentarer: d.edit_standard_kommentar, - navn_rimeligste_alternativ: d.edit_rimeligste_navn, - rimeligste_alternativ: Number(d.edit_rimeligste_pris) || null, - })); - - if (toApprove.length === 0) return alert("Velg minst ett anlegg Γ₯ godkjenne."); - - setSaving(true); - try { - const res = await fetch(`${API_URL}/admin/membership/approve-bulk`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ approvals: toApprove }) - }); - if (res.ok) { - alert(`${toApprove.length} anlegg ble oppdatert og lagret til live!`); - setSelectedIds([]); - fetchDrafts(); // Oppdaterer listen (fjerner de godkjente) - } else { - alert("Noe gikk galt under lagring."); - } - } catch (e) { - alert("Nettverksfeil"); - } - setSaving(false); - }; - - if (loading) return
Laster utkast...
; - - return ( -
-
-
-
- ← Tilbake til oversikten -

Medlemskaps-Vaskeriet

-

GΓ₯ gjennom AI-ens forslag, juster hvis nΓΈdvendig, og godkjenn for Γ₯ publisere. Oppdatert-dato settes automatisk i dag.

-
- -
- - {drafts.length === 0 ? ( -
- 🧹 -

Alt er rent og pent!

-

Ingen ventende forslag fra AI-skraperen akkurat nΓ₯.

-
- ) : ( -
-
- toggleSelectAll(e.target.checked)} - /> - Velg Alle -
- - {drafts.map(draft => ( -
-
-
- toggleOne(draft.id)} - /> -
-
- - {/* OPPDATERT: Navn + ID Badge */} -
-

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

- Sjekk Klubbens Nettside β†— -
- - {draft.membership_draft?.ai_begrunnelse && ( -
- πŸ€– AI Begrunnelse: {draft.membership_draft.ai_begrunnelse} -
- )} - -
- {/* Standard */} -
-

Standard Medlemskap (Ubegrenset)

-
- updateDraftField(draft.id, 'edit_standard_navn', e.target.value)} placeholder="Navn (eks. Hovedmedlem)" /> - updateDraftField(draft.id, 'edit_standard_pris', e.target.value)} placeholder="Pris" /> -
- updateDraftField(draft.id, 'edit_standard_kommentar', e.target.value)} placeholder="Kommentar (F.eks: Inkluderer ikke treningsavgift)" /> -

Gammel pris var: {draft.standard_medlemskap ? `kr ${draft.standard_medlemskap} (${draft.navn_standard_medlemskap})` : 'Ikke registrert'}

-
- - {/* Rimeligste */} -
-

Rimeligste (Betaler Greenfee)

-
- updateDraftField(draft.id, 'edit_rimeligste_navn', e.target.value)} placeholder="Navn (eks. Greenfeemedlem)" /> - updateDraftField(draft.id, 'edit_rimeligste_pris', e.target.value)} placeholder="Pris" /> -
-

Gammel pris var: {draft.rimeligste_alternativ ? `kr ${draft.rimeligste_alternativ} (${draft.navn_rimeligste_alternativ})` : 'Ikke registrert'}

-
-
-
-
-
- ))} -
- )} -
-
- ); -} \ No newline at end of file diff --git a/kode_eksport_1/frontend_src_app_admin_page_tsx.txt b/kode_eksport_1/frontend_src_app_admin_page_tsx.txt deleted file mode 100644 index 1bb56fa..0000000 --- a/kode_eksport_1/frontend_src_app_admin_page_tsx.txt +++ /dev/null @@ -1,468 +0,0 @@ -"use client"; -/** - * TEE OFF ADMIN DASHBOARD v4.0 - KONTROLLPANEL - */ - -import { useState, useEffect, useMemo } from 'react'; -import { API_URL } from "@/config/constants"; -import ScrapeMethodSelect from "@/components/ScrapeMethodSelect"; -import Link from 'next/link'; - -const InlineEdit = ({ facilityId, field, initialValue, onSave }: { facilityId: number, field: string, initialValue: string, onSave: (id: number, field: string, val: string) => void }) => { - const [isEditing, setIsEditing] = useState(false); - const [value, setValue] = useState(initialValue || ''); - - const handleSave = () => { - setIsEditing(false); - if (value !== initialValue) { - onSave(facilityId, field, value); - } - }; - - if (isEditing) { - return ( -
-