Før implementering av meninger, rating og banebesøk på detaljsidene

This commit is contained in:
Erol Haagenrud 2026-04-21 08:21:52 +02:00
parent e2bcd7e21b
commit db478c849e
4 changed files with 123 additions and 47 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,5 +1,5 @@
"use client";
import { useRef, useState, type ChangeEvent } from 'react';
import { useRef, useState, type ChangeEvent, type ReactNode } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { adminFetch } from "@/config/adminFetch";
@ -133,6 +133,81 @@ const ListObjectEditor = ({ label, value, templateKeys, onChange }: { label: str
);
};
const AccordionSection = ({
title,
subtitle,
badge,
defaultOpen = false,
tone = 'gray',
children,
}: {
title: string;
subtitle?: string;
badge?: string;
defaultOpen?: boolean;
tone?: 'gray' | 'green';
children: ReactNode;
}) => {
const tones = {
gray: {
shell: 'border border-gray-200 bg-gray-100',
title: 'text-gray-800',
subtitle: 'text-gray-500',
badge: 'bg-white text-gray-600',
arrow: 'bg-white text-[#11280f]',
content: 'border-t border-gray-200',
},
green: {
shell: 'border border-[#8bc34a]/30 bg-[#8bc34a]/10',
title: 'text-[#11280f]',
subtitle: 'text-[#435340]',
badge: 'bg-white text-[#536256]',
arrow: 'bg-white text-[#11280f]',
content: 'border-t border-[#8bc34a]/20',
},
}[tone];
return (
<details open={defaultOpen} className={`mb-8 overflow-hidden rounded-[2rem] shadow-sm ${tones.shell}`}>
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 p-6 md:p-8 [&::-webkit-details-marker]:hidden">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-3">
<h3 className={`text-sm font-black uppercase tracking-widest ${tones.title}`}>{title}</h3>
{badge ? (
<span className={`rounded-xl px-3 py-2 text-[10px] font-black uppercase tracking-widest ${tones.badge}`}>
{badge}
</span>
) : null}
</div>
{subtitle ? (
<p className={`mt-2 max-w-3xl text-sm leading-6 ${tones.subtitle}`}>{subtitle}</p>
) : null}
</div>
<span className={`shrink-0 rounded-2xl px-4 py-3 text-xs font-black uppercase tracking-widest ${tones.arrow}`}>
Vis / skjul
</span>
</summary>
<div className={`px-6 pb-6 md:px-8 md:pb-8 ${tones.content}`}>
<div className="pt-6">
{children}
</div>
</div>
</details>
);
};
const ScrollToTopButton = () => {
return (
<button
type="button"
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
className="fixed bottom-5 right-5 z-[80] rounded-2xl bg-[#11280f] px-5 py-3 text-xs font-black uppercase tracking-widest text-white shadow-[0_18px_45px_rgba(17,40,15,0.28)] transition-transform hover:-translate-y-0.5 md:bottom-8 md:right-8"
>
Til toppen
</button>
);
};
// KOMPONENT 4: DEN NYE SCOREKORT-BYGGEREN
const ScorecardBuilder = ({ course, onChange }: { course: any, onChange: (c: any) => void }) => {
const ALL_KEYS = ['lengst', 'lang', 'mellomlang', 'mellomkort', 'kort', 'kortest'];
@ -642,6 +717,7 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
return (
<div className="max-w-[1400px] mx-auto p-4 md:p-8 relative z-40 bg-white min-h-screen">
<ScrollToTopButton />
<AdminMobileMenu />
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-10 pb-6 border-b border-gray-200 gap-6">
<div>
@ -821,22 +897,13 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
onChange={handleGalleryUpload}
/>
<div className="mb-8 rounded-[2rem] border border-[#8bc34a]/30 bg-[#8bc34a]/10 p-6 md:p-8">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<h3 className="text-sm font-black uppercase tracking-widest text-[#11280f]">Anleggsbilder</h3>
<p className="mt-2 max-w-3xl text-sm leading-6 text-[#435340]">
Last opp AVIF-optimaliserte bilder direkte fra admin. Du kan også fjerne koblingen til et bilde uten å slette selve filen fra serveren.
</p>
</div>
{mediaFeedback ? (
<div className="rounded-2xl bg-white/90 px-4 py-3 text-sm font-bold text-[#11280f] shadow-sm">
{mediaFeedback}
</div>
) : null}
</div>
<div className="mt-6 grid gap-6 xl:grid-cols-2">
<AccordionSection
title="Anleggsbilder"
subtitle="Last opp AVIF-optimaliserte bilder direkte fra admin. Du kan også fjerne koblingen til et bilde uten å slette selve filen fra serveren."
badge={mediaFeedback || `${galleryImages.length} i galleri`}
tone="green"
>
<div className="grid gap-6 xl:grid-cols-2">
<div className="rounded-[1.75rem] border border-[#11280f]/10 bg-white p-5 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
@ -1017,7 +1084,7 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
</div>
)}
</div>
</div>
</AccordionSection>
<div className="flex flex-col gap-2 mb-8"><label className="text-xs font-black uppercase tracking-widest text-gray-600">Nettside URL</label><input className="p-4 rounded-2xl border-2 border-gray-300 bg-white text-black text-base font-bold shadow-sm focus:border-[#8bc34a] outline-none" value={getValue('website_url', 'text')} onChange={e => handleChange('website_url', e.target.value)} /></div>
<div className="flex flex-col gap-2 mb-8"><label className="text-xs font-black uppercase tracking-widest text-gray-600">Golfbox Booking URL</label><input className="p-4 rounded-2xl border-2 border-gray-300 bg-white text-black text-base font-bold shadow-sm focus:border-[#8bc34a] outline-none" value={getValue('golfbox_booking_url', 'text')} onChange={e => handleChange('golfbox_booking_url', e.target.value)} /></div>
@ -1040,8 +1107,10 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
{activeTab === 'okonomi' && (
<div className="flex flex-col">
{/* MEDLEMSKAP */}
<div className="bg-gray-100 p-6 rounded-2xl mb-8 border border-gray-200">
<h3 className="font-black uppercase tracking-widest text-gray-800 mb-6 pb-2 border-b-2 border-gray-200">Medlemskap</h3>
<AccordionSection
title="Medlemskap"
badge={getValue('membership_updated_at', 'date') || 'Ingen dato'}
>
<div className="flex flex-col gap-2 mb-8">
<label className="text-xs font-black uppercase tracking-widest text-gray-600">Sist Oppdatert (Dato)</label>
<input type="date" className="p-4 rounded-2xl border-2 border-gray-300 bg-white text-black text-base font-bold shadow-sm focus:border-[#8bc34a] outline-none w-max" value={getValue('membership_updated_at', 'date')} onChange={e => handleChange('membership_updated_at', e.target.value)} />
@ -1054,11 +1123,13 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
<div className="flex flex-col gap-2 mb-8"><label className="text-xs font-black uppercase tracking-widest text-gray-600">Pris rimeligste (kun tall)</label><input type="number" className="p-4 rounded-2xl border-2 border-gray-300 bg-white text-black text-base font-bold shadow-sm focus:border-[#8bc34a] outline-none" value={getValue('rimeligste_alternativ', 'number')} onChange={e => handleChange('rimeligste_alternativ', Number(e.target.value))} /></div>
<div className="flex flex-col gap-2 mb-8 col-span-1 md:col-span-2"><label className="text-xs font-black uppercase tracking-widest text-gray-600">Lenke til medlemskapsside</label><input className="p-4 rounded-2xl border-2 border-gray-300 bg-white text-black text-base font-bold shadow-sm focus:border-[#8bc34a] outline-none" value={getValue('medlemskap_url', 'text')} onChange={e => handleChange('medlemskap_url', e.target.value)} /></div>
</div>
</div>
</AccordionSection>
{/* GREENFEE */}
<div className="bg-gray-100 p-6 rounded-2xl border border-gray-200 mb-8">
<h3 className="font-black uppercase tracking-widest text-gray-800 mb-6 pb-2 border-b-2 border-gray-200">Greenfee / Gjestespill</h3>
<AccordionSection
title="Greenfee / Gjestespill"
badge={`${Array.isArray(formData.greenfee) ? formData.greenfee.length : 0} priser`}
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8">
<div className="flex flex-col gap-2 mb-8"><label className="text-xs font-black uppercase tracking-widest text-gray-600">Lenke til Greenfee-side</label><input className="p-4 rounded-2xl border-2 border-gray-300 bg-white text-black text-base font-bold shadow-sm focus:border-[#8bc34a] outline-none" value={getValue('greenfee_url', 'text')} onChange={e => handleChange('greenfee_url', e.target.value)} /></div>
<div className="flex flex-col gap-2 mb-8"><label className="text-xs font-black uppercase tracking-widest text-gray-600">Krav til Gjestespill (f.eks Klubbhandicap)</label><input className="p-4 rounded-2xl border-2 border-gray-300 bg-white text-black text-base font-bold shadow-sm focus:border-[#8bc34a] outline-none" value={getValue('guest_requirements', 'text')} onChange={e => handleChange('guest_requirements', e.target.value)} /></div>
@ -1078,11 +1149,14 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
templateKeys={['banenavn', 'priskategori', 'pris_voksne', 'pris_junior']}
onChange={(v) => handleChange('greenfee', v)}
/>
</div>
</AccordionSection>
{/* VEIEN TIL GOLF (VTG) */}
<div className="bg-[#8bc34a]/10 p-6 rounded-2xl border border-[#8bc34a]/30 mb-8">
<h3 className="font-black uppercase tracking-widest text-[#11280f] mb-6 pb-2 border-b-2 border-[#8bc34a]/20">Veien til Golf (VTG)</h3>
<AccordionSection
title="Veien til Golf (VTG)"
badge={getValue('vtg_pris', 'number') ? `${getValue('vtg_pris', 'number')} kr` : 'Ingen pris'}
tone="green"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8">
<div className="flex flex-col gap-2 mb-8"><label className="text-xs font-black uppercase tracking-widest text-gray-600">Pris VTG kurs (kun tall)</label><input type="number" className="p-4 rounded-2xl border-2 border-gray-300 bg-white text-black text-base font-bold shadow-sm focus:border-[#8bc34a] outline-none" value={getValue('vtg_pris', 'number')} onChange={e => handleChange('vtg_pris', Number(e.target.value))} /></div>
<div className="flex flex-col gap-2 mb-8"><label className="text-xs font-black uppercase tracking-widest text-gray-600">Lenke til VTG påmelding</label><input className="p-4 rounded-2xl border-2 border-gray-300 bg-white text-black text-base font-bold shadow-sm focus:border-[#8bc34a] outline-none" value={getValue('vtg_lenke', 'text')} onChange={e => handleChange('vtg_lenke', e.target.value)} /></div>
@ -1094,7 +1168,7 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
templateKeys={['dato', 'status']}
onChange={(v) => handleChange('vtg_datoer', v)}
/>
</div>
</AccordionSection>
<div className="mt-8 border-t-2 border-gray-200 pt-8">
<KeyValueEditor label="Fasiliteter (Proshop, Kafé etc.)" value={formData.amenities} onChange={(v) => handleChange('amenities', v)} />
@ -1144,25 +1218,27 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
</div>
{formData.courses?.map((course: any, cIdx: number) => (
<div key={course.id || course._clientId || cIdx} className="bg-gray-100 p-8 rounded-[2rem] border-2 border-gray-200 shadow-sm mb-8">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4 border-b-2 border-gray-200 pb-4">
<div className="flex flex-col gap-3">
<h4 className="text-2xl font-black text-black">{course.name}</h4>
<div className="flex flex-wrap items-center gap-3">
<label className="inline-flex items-center gap-3 rounded-xl bg-white px-4 py-2 shadow-sm">
<input
type="radio"
name="main-course"
checked={Boolean(course.is_main_course)}
onChange={() => handleSetMainCourse(cIdx)}
className="h-4 w-4 accent-[#8bc34a]"
/>
<span className="text-xs font-black uppercase tracking-widest text-[#11280f]">Hovedbane</span>
</label>
<span className={`px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest ${course.is_main_course ? 'bg-[#8bc34a] text-white shadow-md' : 'bg-gray-300 text-gray-700'}`}>
{course.is_main_course ? 'Hovedbane' : 'Sekundærbane'}
</span>
</div>
<AccordionSection
key={course.id || course._clientId || cIdx}
title={course.name || `Bane ${cIdx + 1}`}
subtitle={course.is_main_course ? 'Hovedbane' : 'Sekundærbane'}
badge={`${course.holes?.length || 0} hull`}
>
<div className="mb-8 flex flex-col md:flex-row justify-between items-start md:items-center gap-4 border-b-2 border-gray-200 pb-4">
<div className="flex flex-wrap items-center gap-3">
<label className="inline-flex items-center gap-3 rounded-xl bg-white px-4 py-2 shadow-sm">
<input
type="radio"
name="main-course"
checked={Boolean(course.is_main_course)}
onChange={() => handleSetMainCourse(cIdx)}
className="h-4 w-4 accent-[#8bc34a]"
/>
<span className="text-xs font-black uppercase tracking-widest text-[#11280f]">Hovedbane</span>
</label>
<span className={`px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest ${course.is_main_course ? 'bg-[#8bc34a] text-white shadow-md' : 'bg-gray-300 text-gray-700'}`}>
{course.is_main_course ? 'Hovedbane' : 'Sekundærbane'}
</span>
</div>
<button onClick={() => handleRemoveCourse(cIdx)} className="btn btn-md btn-danger w-full md:w-auto">Slett bane</button>
</div>
@ -1228,7 +1304,7 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
});
}}
/>
</div>
</AccordionSection>
))}
</div>
)}