Files
GearBox/src/client/components/CreateThreadModal.tsx
Jean-Luc Makiola 9bcdcc7168 style: replace blue accent with gray and mute card badge colors
Switch all interactive UI elements (buttons, focus rings, active tabs,
FAB, links, spinners) from blue to gray to match icon colors for a
more cohesive look. Mute card badge text colors to pastels (blue-400,
green-500, purple-500) to keep the focus on card content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 22:42:38 +01:00

144 lines
3.8 KiB
TypeScript

import { useEffect, useState } from "react";
import { useCategories } from "../hooks/useCategories";
import { useCreateThread } from "../hooks/useThreads";
import { useUIStore } from "../stores/uiStore";
export function CreateThreadModal() {
const isOpen = useUIStore((s) => s.createThreadModalOpen);
const closeModal = useUIStore((s) => s.closeCreateThreadModal);
const { data: categories } = useCategories();
const createThread = useCreateThread();
const [name, setName] = useState("");
const [categoryId, setCategoryId] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
// Pre-select first category when categories load
useEffect(() => {
if (categories && categories.length > 0 && categoryId === null) {
setCategoryId(categories[0].id);
}
}, [categories, categoryId]);
if (!isOpen) return null;
function resetForm() {
setName("");
setCategoryId(categories?.[0]?.id ?? null);
setError(null);
}
function handleClose() {
resetForm();
closeModal();
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const trimmed = name.trim();
if (!trimmed) {
setError("Thread name is required");
return;
}
if (categoryId === null) {
setError("Please select a category");
return;
}
setError(null);
createThread.mutate(
{ name: trimmed, categoryId },
{
onSuccess: () => {
resetForm();
closeModal();
},
onError: (err) => {
setError(
err instanceof Error ? err.message : "Failed to create thread",
);
},
},
);
}
return (
<div
role="dialog"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
onClick={handleClose}
onKeyDown={(e) => {
if (e.key === "Escape") handleClose();
}}
>
<div
role="document"
className="w-full max-w-md bg-white rounded-xl shadow-xl p-6"
onClick={(e) => e.stopPropagation()}
onKeyDown={() => {}}
>
<h2 className="text-lg font-semibold text-gray-900 mb-4">New Thread</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="thread-name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Thread name
</label>
<input
id="thread-name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Lightweight sleeping bag"
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent"
/>
</div>
<div>
<label
htmlFor="thread-category"
className="block text-sm font-medium text-gray-700 mb-1"
>
Category
</label>
<select
id="thread-category"
value={categoryId ?? ""}
onChange={(e) => setCategoryId(Number(e.target.value))}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent bg-white"
>
{categories?.map((cat) => (
<option key={cat.id} value={cat.id}>
{cat.name}
</option>
))}
</select>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
<div className="flex justify-end gap-2 pt-2">
<button
type="button"
onClick={handleClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={createThread.isPending}
className="px-4 py-2 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 disabled:opacity-50 rounded-lg transition-colors"
>
{createThread.isPending ? "Creating..." : "Create Thread"}
</button>
</div>
</form>
</div>
</div>
);
}