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