Før implementering av meninger, rating og banebesøk på detaljsidene
This commit is contained in:
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 |
BIN
frontend/public/TeeOff.no.png
Normal file
BIN
frontend/public/TeeOff.no.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue