Files
GearBox/src/client/components/CreateThreadModal.tsx
Jean-Luc Makiola bea386e7db
All checks were successful
CI / ci (push) Successful in 1m21s
CI / e2e (push) Has been skipped
CI / deploy (push) Successful in 1m15s
style(i18n): fix lint — formatting and import ordering across 21 files
Biome auto-fix for formatting (line length, ternary wrapping) and
import organization in files touched by phase 34 i18n work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 14:49:10 +02:00

150 lines
4.0 KiB
TypeScript

import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useCategories } from "../hooks/useCategories";
import { useCreateThread } from "../hooks/useThreads";
import { useUIStore } from "../stores/uiStore";
export function CreateThreadModal() {
const { t } = useTranslation(["threads", "common"]);
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(t("create.nameRequired"));
return;
}
if (categoryId === null) {
setError(t("create.selectCategory"));
return;
}
setError(null);
createThread.mutate(
{ name: trimmed, categoryId },
{
onSuccess: () => {
resetForm();
closeModal();
},
onError: (err) => {
setError(
err instanceof Error ? err.message : t("create.createFailed"),
);
},
},
);
}
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">
{t("create.title")}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="thread-name"
className="block text-sm font-medium text-gray-700 mb-1"
>
{t("create.threadName")}
</label>
<input
id="thread-name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t("create.namePlaceholder")}
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"
>
{t("create.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"
>
{t("common:actions.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
? t("common:actions.creating")
: t("create.createThread")}
</button>
</div>
</form>
</div>
</div>
);
}