| @@ -1,9 +1,66 @@ | |||
| 'use client'; | |||
| import { useState, useEffect } from 'react'; | |||
| import { Card, Portal, MediaItem } from '@/types'; | |||
| import { useState, useEffect, useRef } from 'react'; | |||
| import { Card, Portal, MediaItem, CardType } from '@/types'; | |||
| import { EXTERNAL_LINK_ENABLED } from '@/lib/config'; | |||
| function CardTypeSelect({ | |||
| value, | |||
| onChange, | |||
| options, | |||
| }: { | |||
| value: CardType; | |||
| onChange: (v: CardType) => void; | |||
| options: { value: CardType; label: string }[]; | |||
| }) { | |||
| const [open, setOpen] = useState(false); | |||
| const ref = useRef<HTMLDivElement>(null); | |||
| useEffect(() => { | |||
| if (!open) return; | |||
| const onClick = (e: MouseEvent) => { | |||
| if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); | |||
| }; | |||
| const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); }; | |||
| document.addEventListener('mousedown', onClick); | |||
| document.addEventListener('keydown', onKey); | |||
| return () => { | |||
| document.removeEventListener('mousedown', onClick); | |||
| document.removeEventListener('keydown', onKey); | |||
| }; | |||
| }, [open]); | |||
| const current = options.find(o => o.value === value); | |||
| const inputBase = "w-full border border-gray-300 p-2.5 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"; | |||
| return ( | |||
| <div ref={ref} className="relative"> | |||
| <button | |||
| type="button" | |||
| onClick={() => setOpen(o => !o)} | |||
| className={`${inputBase} text-left flex items-center justify-between cursor-pointer`} | |||
| > | |||
| <span>{current?.label || ''}</span> | |||
| <span className={`text-gray-500 transition-transform ${open ? 'rotate-180' : ''}`}>▾</span> | |||
| </button> | |||
| {open && ( | |||
| <div className="absolute left-0 right-0 top-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-30 overflow-hidden"> | |||
| {options.map(o => ( | |||
| <button | |||
| key={o.value} | |||
| type="button" | |||
| onClick={() => { onChange(o.value); setOpen(false); }} | |||
| className={`w-full text-left px-3 py-2.5 hover:bg-blue-50 transition-colors ${o.value === value ? 'bg-blue-100 font-semibold text-blue-700' : 'text-gray-800'}`} | |||
| > | |||
| {o.label} | |||
| </button> | |||
| ))} | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| const isVideoUrl = (url: string) => /\.(mp4|webm|mov|m4v|ogv)(\?|$)/i.test(url); | |||
| const isPdfFile = (file: File) => | |||
| file.type === 'application/pdf' || /\.pdf$/i.test(file.name); | |||
| @@ -496,11 +553,15 @@ export default function AdminDashboard() { | |||
| </div> | |||
| <div> | |||
| <label className="block text-sm font-semibold text-gray-800 mb-1">Card Type</label> | |||
| <select value={isEditing.cardType || 'INFO_PAGE'} onChange={e => setIsEditing({...isEditing, cardType: e.target.value as any})} className={inputClasses}> | |||
| <option value="INFO_PAGE">Info Page</option> | |||
| <option value="IMAGE_GALLERY">Image Gallery</option> | |||
| {EXTERNAL_LINK_ENABLED && <option value="EXTERNAL_LINK">External Link</option>} | |||
| </select> | |||
| <CardTypeSelect | |||
| value={(isEditing.cardType || 'INFO_PAGE') as CardType} | |||
| onChange={(v) => setIsEditing({ ...isEditing, cardType: v })} | |||
| options={[ | |||
| { value: 'INFO_PAGE', label: 'Info Page' }, | |||
| { value: 'IMAGE_GALLERY', label: 'Image Gallery' }, | |||
| ...(EXTERNAL_LINK_ENABLED ? [{ value: 'EXTERNAL_LINK' as CardType, label: 'External Link' }] : []), | |||
| ]} | |||
| /> | |||
| </div> | |||
| {isEditing.cardType === 'EXTERNAL_LINK' ? ( | |||
| <> | |||