diff --git a/web/src/components/TopBandBar.tsx b/web/src/components/TopBandBar.tsx index 19dd60e..7be8c28 100644 --- a/web/src/components/TopBandBar.tsx +++ b/web/src/components/TopBandBar.tsx @@ -1,14 +1,215 @@ import { useRef, useState, useEffect } from "react"; import { useNavigate, useLocation, matchPath } from "react-router-dom"; -import { useQuery } from "@tanstack/react-query"; -import { listBands } from "../api/bands"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { listBands, createBand } from "../api/bands"; import { getInitials } from "../utils"; import { useBandStore } from "../stores/bandStore"; +// ── Create Band Modal ────────────────────────────────────────────────────────── + +function CreateBandModal({ onClose }: { onClose: () => void }) { + const navigate = useNavigate(); + const qc = useQueryClient(); + const [name, setName] = useState(""); + const [slug, setSlug] = useState(""); + const [ncFolder, setNcFolder] = useState(""); + const [error, setError] = useState(null); + const nameRef = useRef(null); + + useEffect(() => { + nameRef.current?.focus(); + }, []); + + // Close on Escape + useEffect(() => { + const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; + document.addEventListener("keydown", handler); + return () => document.removeEventListener("keydown", handler); + }, [onClose]); + + const mutation = useMutation({ + mutationFn: () => + createBand({ + name, + slug, + ...(ncFolder.trim() ? { nc_base_path: ncFolder.trim() } : {}), + }), + onSuccess: (band) => { + qc.invalidateQueries({ queryKey: ["bands"] }); + onClose(); + navigate(`/bands/${band.id}`); + }, + onError: (err) => { + setError(err instanceof Error ? err.message : "Failed to create band"); + }, + }); + + const handleNameChange = (v: string) => { + setName(v); + setSlug( + v.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "") + ); + }; + + const inputStyle: React.CSSProperties = { + width: "100%", + padding: "8px 11px", + background: "rgba(255,255,255,0.04)", + border: "1px solid rgba(255,255,255,0.1)", + borderRadius: 7, + color: "#e8e9f0", + fontSize: 13, + fontFamily: "inherit", + outline: "none", + boxSizing: "border-box", + }; + + const labelStyle: React.CSSProperties = { + display: "block", + fontSize: 10, + fontWeight: 600, + letterSpacing: "0.06em", + color: "rgba(232,233,240,0.4)", + marginBottom: 5, + }; + + return ( + /* Backdrop */ +
+ {/* Dialog */} +
e.stopPropagation()} + style={{ + background: "#112018", + border: "1px solid rgba(255,255,255,0.1)", + borderRadius: 14, + padding: 28, + width: 400, + boxShadow: "0 24px 64px rgba(0,0,0,0.6)", + }} + > +

+ New band +

+

+ Create a workspace for your recordings. +

+ + {error && ( +

+ {error} +

+ )} + +
+ + handleNameChange(e.target.value)} + style={inputStyle} + placeholder="e.g. The Midnight Trio" + onKeyDown={(e) => { if (e.key === "Enter" && name && slug) mutation.mutate(); }} + /> +
+ +
+ + setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""))} + style={{ ...inputStyle, fontFamily: "monospace" }} + placeholder="the-midnight-trio" + /> +
+ + {/* NC folder section */} +
+ + setNcFolder(e.target.value)} + style={{ ...inputStyle, fontFamily: "monospace" }} + placeholder={slug ? `bands/${slug}/` : "bands/my-band/"} + /> +

+ Path relative to your Nextcloud root. Leave blank to auto-create{" "} + + bands/{slug || "slug"}/ + + .{" "} + Nextcloud must be configured in{" "} + + Settings → Storage + + . +

+
+ + {/* Actions */} +
+ + +
+
+
+ ); +} + +// ── TopBandBar ───────────────────────────────────────────────────────────────── + export function TopBandBar() { const navigate = useNavigate(); const location = useLocation(); const [open, setOpen] = useState(false); + const [showCreate, setShowCreate] = useState(false); const ref = useRef(null); const { data: bands } = useQuery({ queryKey: ["bands"], queryFn: listBands }); @@ -27,7 +228,7 @@ export function TopBandBar() { const currentBandId = urlBandId ?? activeBandId; const activeBand = bands?.find((b) => b.id === currentBandId) ?? null; - // Close on outside click + // Close dropdown on outside click useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { @@ -40,110 +241,114 @@ export function TopBandBar() { const border = "rgba(255,255,255,0.06)"; return ( -
- {/* Band switcher */} -
- + <> + {showCreate && setShowCreate(false)} />} - {open && ( -
- {bands?.map((band) => ( - - ))} + {activeBand.name} + + ) : ( + Select a band + )} + + + + -
- + {open && ( +
+ {bands?.map((band) => ( + + ))} + +
+ +
-
- )} + )} +
-
+ ); }