171 lines
6.9 KiB
JavaScript
171 lines
6.9 KiB
JavaScript
import { useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { getShowEvents, createShowEvent, addShowToEvent } from '../api'
|
|
import Button from './Button'
|
|
|
|
// Modal launched from a row's "Add to Event" button in the Shows browser.
|
|
// Lets the user drop the ARBA show into an existing event or spin up a new one
|
|
// inline (design doc §6 "Adding Shows to an Event Flow").
|
|
function AddToEventModal({ show, onClose }) {
|
|
const queryClient = useQueryClient()
|
|
const { data: events, isLoading } = useQuery({ queryKey: ['show-events'], queryFn: getShowEvents })
|
|
|
|
const [mode, setMode] = useState('existing') // 'existing' | 'new'
|
|
const [selectedId, setSelectedId] = useState('')
|
|
const [newName, setNewName] = useState('')
|
|
const [newNotes, setNewNotes] = useState('')
|
|
const [result, setResult] = useState(null) // { id, name } once added
|
|
|
|
// No events yet → force the create path.
|
|
const hasEvents = (events?.length ?? 0) > 0
|
|
const effectiveMode = hasEvents ? mode : 'new'
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: async () => {
|
|
let eventId
|
|
let eventName
|
|
if (effectiveMode === 'new') {
|
|
const created = await createShowEvent({ name: newName.trim(), notes: newNotes.trim() || null })
|
|
eventId = created.id
|
|
eventName = created.name
|
|
} else {
|
|
eventId = Number(selectedId)
|
|
eventName = events.find((e) => e.id === eventId)?.name ?? 'event'
|
|
}
|
|
await addShowToEvent(eventId, show)
|
|
return { id: eventId, name: eventName }
|
|
},
|
|
onSuccess: (added) => {
|
|
setResult(added)
|
|
queryClient.invalidateQueries({ queryKey: ['show-events'] })
|
|
queryClient.invalidateQueries({ queryKey: ['show-event', added.id] })
|
|
},
|
|
})
|
|
|
|
const canSubmit =
|
|
effectiveMode === 'new' ? newName.trim().length > 0 : selectedId !== ''
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-30 flex items-center justify-center bg-black/40 p-4"
|
|
onMouseDown={(e) => {
|
|
if (e.target === e.currentTarget) onClose()
|
|
}}
|
|
>
|
|
<div className="w-full max-w-md rounded-lg border border-border bg-surface-raised p-6 shadow-xl">
|
|
{result ? (
|
|
<div className="flex flex-col gap-4">
|
|
<h2 className="font-display text-lg text-text-primary">Added to event</h2>
|
|
<p className="font-body text-sm text-text-secondary">
|
|
<span className="font-medium text-text-primary">{show.club_name}</span> was added to{' '}
|
|
<span className="font-medium text-text-primary">{result.name}</span>.
|
|
</p>
|
|
<div className="flex justify-end gap-3">
|
|
<Button variant="secondary" onClick={onClose}>
|
|
Close
|
|
</Button>
|
|
<Link to={`/shows/events/${result.id}`}>
|
|
<Button variant="primary">View Event</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<h2 className="font-display text-lg text-text-primary">Add to Event</h2>
|
|
<p className="mt-1 font-body text-sm text-text-secondary">
|
|
{show.club_name} — {show.raw_date || show.date}
|
|
</p>
|
|
</div>
|
|
|
|
{hasEvents && (
|
|
<div className="flex gap-1 rounded-md border border-border bg-surface p-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => setMode('existing')}
|
|
className={`flex-1 rounded px-3 py-1.5 font-body text-sm ${
|
|
effectiveMode === 'existing' ? 'bg-accent text-surface' : 'text-text-primary hover:bg-surface-sunken'
|
|
}`}
|
|
>
|
|
Existing event
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setMode('new')}
|
|
className={`flex-1 rounded px-3 py-1.5 font-body text-sm ${
|
|
effectiveMode === 'new' ? 'bg-accent text-surface' : 'text-text-primary hover:bg-surface-sunken'
|
|
}`}
|
|
>
|
|
New event
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{effectiveMode === 'existing' ? (
|
|
<div>
|
|
<label className="block font-body text-sm font-medium text-text-primary">Event</label>
|
|
<select
|
|
value={selectedId}
|
|
onChange={(e) => setSelectedId(e.target.value)}
|
|
className="mt-1 w-full rounded-md border border-border bg-surface-raised px-3 py-2 font-body text-sm focus:outline-none focus:ring-2 focus:ring-accent"
|
|
>
|
|
<option value="">{isLoading ? 'Loading…' : 'Select an event'}</option>
|
|
{events?.map((event) => (
|
|
<option key={event.id} value={event.id}>
|
|
{event.name} ({event.show_count} show{event.show_count === 1 ? '' : 's'})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col gap-3">
|
|
<div>
|
|
<label className="block font-body text-sm font-medium text-text-primary">
|
|
Event name <span className="text-danger">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={newName}
|
|
onChange={(e) => setNewName(e.target.value)}
|
|
placeholder="e.g. Spring Circuit Weekend"
|
|
className="mt-1 w-full rounded-md border border-border px-3 py-2 font-body text-sm focus:outline-none focus:ring-2 focus:ring-accent"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block font-body text-sm font-medium text-text-primary">Notes</label>
|
|
<textarea
|
|
value={newNotes}
|
|
onChange={(e) => setNewNotes(e.target.value)}
|
|
rows={2}
|
|
className="mt-1 w-full rounded-md border border-border px-3 py-2 font-body text-sm focus:outline-none focus:ring-2 focus:ring-accent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{mutation.isError && (
|
|
<p className="font-body text-sm text-danger">{mutation.error.message}</p>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-3">
|
|
<Button variant="secondary" onClick={onClose} disabled={mutation.isPending}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => mutation.mutate()}
|
|
disabled={!canSubmit || mutation.isPending}
|
|
>
|
|
{mutation.isPending ? 'Adding…' : 'Add'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default AddToEventModal
|