| | | 1 | | import { useEffect, useRef, useState } from "react"; |
| | | 2 | | import { useFetcher } from "react-router"; |
| | | 3 | | import type { LocationResponse, RoomResponse } from "~/api/client"; |
| | | 4 | | import { FormField, PanelHeader } from "~/components/panel-ui"; |
| | | 5 | | import { inputStyle } from "~/lib/styles"; |
| | | 6 | | |
| | | 7 | | type ActionData = |
| | | 8 | | | { ok: true; intent: "create-room"; room: RoomResponse } |
| | | 9 | | | { ok: false; error: string }; |
| | | 10 | | |
| | 0 | 11 | | export function RoomFormPanel({ locations, defaultLocationId, onClose, onCreated }: { |
| | | 12 | | locations: LocationResponse[]; |
| | | 13 | | defaultLocationId?: number; |
| | | 14 | | onClose: () => void; |
| | | 15 | | onCreated: (room: RoomResponse) => void; |
| | | 16 | | }) { |
| | 0 | 17 | | const fetcher = useFetcher<ActionData>(); |
| | 0 | 18 | | const [validationError, setValidationError] = useState<string | null>(null); |
| | 0 | 19 | | const submitting = fetcher.state !== "idle"; |
| | | 20 | | |
| | 0 | 21 | | const actionError = fetcher.state === "idle" && fetcher.data && !fetcher.data.ok |
| | | 22 | | ? fetcher.data.error : null; |
| | 0 | 23 | | const error = validationError ?? actionError; |
| | | 24 | | |
| | 0 | 25 | | const onCreatedRef = useRef(onCreated); |
| | 0 | 26 | | useEffect(() => { onCreatedRef.current = onCreated; }); |
| | | 27 | | |
| | 0 | 28 | | const fetchedRef = useRef(false); |
| | 0 | 29 | | useEffect(() => { |
| | 0 | 30 | | if (fetcher.state === "submitting") { fetchedRef.current = true; return; } |
| | 0 | 31 | | if (!fetchedRef.current || fetcher.state !== "idle" || !fetcher.data) return; |
| | 0 | 32 | | fetchedRef.current = false; |
| | 0 | 33 | | const data = fetcher.data; |
| | 0 | 34 | | if (!data.ok) return; |
| | 0 | 35 | | onCreatedRef.current(data.room); |
| | | 36 | | }, [fetcher.state, fetcher.data]); |
| | | 37 | | |
| | 0 | 38 | | function handleSubmit(e: React.FormEvent<HTMLFormElement>) { |
| | 0 | 39 | | e.preventDefault(); |
| | 0 | 40 | | const fd = new FormData(e.currentTarget); |
| | 0 | 41 | | const name = (fd.get("name") as string ?? "").trim(); |
| | 0 | 42 | | const locationId = Number(fd.get("locationId")); |
| | 0 | 43 | | if (!name) { setValidationError("Name is required."); return; } |
| | 0 | 44 | | if (!locationId) { setValidationError("Select a location."); return; } |
| | 0 | 45 | | setValidationError(null); |
| | 0 | 46 | | fetcher.submit(fd, { method: "post" }); |
| | | 47 | | } |
| | | 48 | | |
| | 0 | 49 | | return ( |
| | | 50 | | <aside className="tui-panel" style={{ |
| | | 51 | | width: 340, borderLeft: "1px solid var(--c-border)", |
| | | 52 | | background: "var(--c-bg-2)", flexShrink: 0, |
| | | 53 | | display: "flex", flexDirection: "column", overflowY: "auto", |
| | | 54 | | }}> |
| | | 55 | | <span className="tui-panel-title">─[ new room ]─</span> |
| | | 56 | | <PanelHeader label="NEW ROOM" onClose={onClose} /> |
| | | 57 | | <form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: 14, padding: 16 }}> |
| | | 58 | | <input type="hidden" name="_intent" value="create-room" /> |
| | | 59 | | {error && <div style={{ fontSize: 12, color: "#ef4444", padding: "6px 10px", background: "rgba(239,68,68,0.08)", |
| | | 60 | | <FormField label="Location"> |
| | | 61 | | <select name="locationId" defaultValue={defaultLocationId ?? locations[0]?.id ?? ""} style={inputStyle}> |
| | 0 | 62 | | {locations.map(loc => ( |
| | 0 | 63 | | <option key={loc.id} value={loc.id}>{loc.name}</option> |
| | | 64 | | ))} |
| | | 65 | | </select> |
| | | 66 | | </FormField> |
| | | 67 | | <FormField label="Name *"> |
| | | 68 | | <input name="name" type="text" required autoFocus placeholder="e.g. Garage, Living Room" style={inputStyle} /> |
| | | 69 | | </FormField> |
| | | 70 | | <FormField label="Description"> |
| | | 71 | | <textarea name="description" rows={2} placeholder="e.g. North-facing bedroom" style={{ ...inputStyle, resize: |
| | | 72 | | </FormField> |
| | | 73 | | <div style={{ display: "flex", gap: 8 }}> |
| | | 74 | | <button type="submit" disabled={submitting} style={{ |
| | | 75 | | flex: 1, padding: "8px 14px", borderRadius: 6, border: "none", |
| | | 76 | | background: "var(--c-accent)", color: "#fff", fontSize: 13, fontWeight: 500, |
| | | 77 | | cursor: submitting ? "not-allowed" : "pointer", opacity: submitting ? 0.7 : 1, fontFamily: "inherit", |
| | | 78 | | }}> |
| | | 79 | | {submitting ? "Creating…" : "Create room"} |
| | | 80 | | </button> |
| | | 81 | | <button type="button" onClick={onClose} disabled={submitting} style={{ |
| | | 82 | | padding: "8px 14px", borderRadius: 6, border: "1px solid var(--c-border)", |
| | | 83 | | background: "transparent", color: "var(--c-fg-2)", fontSize: 13, cursor: "pointer", fontFamily: "inherit", |
| | | 84 | | }}>Cancel</button> |
| | | 85 | | </div> |
| | | 86 | | <p style={{ fontSize: 11, color: "var(--c-fg-3)", margin: 0 }}> |
| | | 87 | | After creating the room you'll be able to add items to it. |
| | | 88 | | </p> |
| | | 89 | | </form> |
| | | 90 | | </aside> |
| | | 91 | | ); |
| | | 92 | | } |