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";
|
"use client";
|
||||||
import { useRef, useState, type ChangeEvent } from 'react';
|
import { useRef, useState, type ChangeEvent, type ReactNode } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { adminFetch } from "@/config/adminFetch";
|
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
|
// KOMPONENT 4: DEN NYE SCOREKORT-BYGGEREN
|
||||||
const ScorecardBuilder = ({ course, onChange }: { course: any, onChange: (c: any) => void }) => {
|
const ScorecardBuilder = ({ course, onChange }: { course: any, onChange: (c: any) => void }) => {
|
||||||
const ALL_KEYS = ['lengst', 'lang', 'mellomlang', 'mellomkort', 'kort', 'kortest'];
|
const ALL_KEYS = ['lengst', 'lang', 'mellomlang', 'mellomkort', 'kort', 'kortest'];
|
||||||
|
|
@ -642,6 +717,7 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-[1400px] mx-auto p-4 md:p-8 relative z-40 bg-white min-h-screen">
|
<div className="max-w-[1400px] mx-auto p-4 md:p-8 relative z-40 bg-white min-h-screen">
|
||||||
|
<ScrollToTopButton />
|
||||||
<AdminMobileMenu />
|
<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 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>
|
<div>
|
||||||
|
|
@ -821,22 +897,13 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
||||||
onChange={handleGalleryUpload}
|
onChange={handleGalleryUpload}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mb-8 rounded-[2rem] border border-[#8bc34a]/30 bg-[#8bc34a]/10 p-6 md:p-8">
|
<AccordionSection
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
title="Anleggsbilder"
|
||||||
<div>
|
subtitle="Last opp AVIF-optimaliserte bilder direkte fra admin. Du kan også fjerne koblingen til et bilde uten å slette selve filen fra serveren."
|
||||||
<h3 className="text-sm font-black uppercase tracking-widest text-[#11280f]">Anleggsbilder</h3>
|
badge={mediaFeedback || `${galleryImages.length} i galleri`}
|
||||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-[#435340]">
|
tone="green"
|
||||||
Last opp AVIF-optimaliserte bilder direkte fra admin. Du kan også fjerne koblingen til et bilde uten å slette selve filen fra serveren.
|
>
|
||||||
</p>
|
<div className="grid gap-6 xl:grid-cols-2">
|
||||||
</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">
|
|
||||||
<div className="rounded-[1.75rem] border border-[#11280f]/10 bg-white p-5 shadow-sm">
|
<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 className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -1017,7 +1084,7 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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">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>
|
<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' && (
|
{activeTab === 'okonomi' && (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{/* MEDLEMSKAP */}
|
{/* MEDLEMSKAP */}
|
||||||
<div className="bg-gray-100 p-6 rounded-2xl mb-8 border border-gray-200">
|
<AccordionSection
|
||||||
<h3 className="font-black uppercase tracking-widest text-gray-800 mb-6 pb-2 border-b-2 border-gray-200">Medlemskap</h3>
|
title="Medlemskap"
|
||||||
|
badge={getValue('membership_updated_at', 'date') || 'Ingen dato'}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-2 mb-8">
|
<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>
|
<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)} />
|
<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"><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 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>
|
||||||
</div>
|
</AccordionSection>
|
||||||
|
|
||||||
{/* GREENFEE */}
|
{/* GREENFEE */}
|
||||||
<div className="bg-gray-100 p-6 rounded-2xl border border-gray-200 mb-8">
|
<AccordionSection
|
||||||
<h3 className="font-black uppercase tracking-widest text-gray-800 mb-6 pb-2 border-b-2 border-gray-200">Greenfee / Gjestespill</h3>
|
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="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">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>
|
<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']}
|
templateKeys={['banenavn', 'priskategori', 'pris_voksne', 'pris_junior']}
|
||||||
onChange={(v) => handleChange('greenfee', v)}
|
onChange={(v) => handleChange('greenfee', v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</AccordionSection>
|
||||||
|
|
||||||
{/* VEIEN TIL GOLF (VTG) */}
|
{/* VEIEN TIL GOLF (VTG) */}
|
||||||
<div className="bg-[#8bc34a]/10 p-6 rounded-2xl border border-[#8bc34a]/30 mb-8">
|
<AccordionSection
|
||||||
<h3 className="font-black uppercase tracking-widest text-[#11280f] mb-6 pb-2 border-b-2 border-[#8bc34a]/20">Veien til Golf (VTG)</h3>
|
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="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">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>
|
<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']}
|
templateKeys={['dato', 'status']}
|
||||||
onChange={(v) => handleChange('vtg_datoer', v)}
|
onChange={(v) => handleChange('vtg_datoer', v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</AccordionSection>
|
||||||
|
|
||||||
<div className="mt-8 border-t-2 border-gray-200 pt-8">
|
<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)} />
|
<KeyValueEditor label="Fasiliteter (Proshop, Kafé etc.)" value={formData.amenities} onChange={(v) => handleChange('amenities', v)} />
|
||||||
|
|
@ -1144,10 +1218,13 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formData.courses?.map((course: any, cIdx: number) => (
|
{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">
|
<AccordionSection
|
||||||
<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">
|
key={course.id || course._clientId || cIdx}
|
||||||
<div className="flex flex-col gap-3">
|
title={course.name || `Bane ${cIdx + 1}`}
|
||||||
<h4 className="text-2xl font-black text-black">{course.name}</h4>
|
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">
|
<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">
|
<label className="inline-flex items-center gap-3 rounded-xl bg-white px-4 py-2 shadow-sm">
|
||||||
<input
|
<input
|
||||||
|
|
@ -1163,7 +1240,6 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
||||||
{course.is_main_course ? 'Hovedbane' : 'Sekundærbane'}
|
{course.is_main_course ? 'Hovedbane' : 'Sekundærbane'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<button onClick={() => handleRemoveCourse(cIdx)} className="btn btn-md btn-danger w-full md:w-auto">Slett bane</button>
|
<button onClick={() => handleRemoveCourse(cIdx)} className="btn btn-md btn-danger w-full md:w-auto">Slett bane</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1228,7 +1304,7 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</AccordionSection>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue