tahamajs/IE / Projects /front /src /components /threads /CreateThreadModal.tsx
tahamajs's picture
download
raw
7.46 kB
// src/components/threads/CreateThreadModal.tsx
import { useState } from "react";
import type { ThreadType } from "@/types/thread";
import { X, Hash, MessageSquare, Lightbulb, Tag, Loader2 } from "lucide-react";
import { toast } from "react-toastify";
interface CreateThreadModalProps {
isOpen: boolean;
onClose: () => void;
onCreate: (data: {
title: string;
type: ThreadType;
parentMessageId?: number;
initialMessage?: string;
tags?: string[];
}) => Promise<void>; // now async
parentMessageId?: number;
parentMessagePreview?: string;
}
const THREAD_TYPES: { value: ThreadType; icon: typeof Hash; label: string; desc: string }[] = [
{
value: "continuation",
icon: MessageSquare,
label: "Continuation",
desc: "Continue discussing a specific message",
},
{
value: "standalone",
icon: Hash,
label: "Standalone Topic",
desc: "Start a new discussion topic",
},
{
value: "topic",
icon: Lightbulb,
label: "Project Thread",
desc: "Organize around a project or paper",
},
];
export function CreateThreadModal({
isOpen,
onClose,
onCreate,
parentMessageId,
parentMessagePreview,
}: CreateThreadModalProps) {
const [title, setTitle] = useState("");
const [type, setType] = useState<ThreadType>("continuation");
const [initialMessage, setInitialMessage] = useState("");
const [tagInput, setTagInput] = useState("");
const [tags, setTags] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
if (!isOpen) return null;
const resetForm = () => {
setTitle("");
setType("continuation");
setInitialMessage("");
setTags([]);
setTagInput("");
};
const handleCreate = async () => {
if (!title.trim()) {
toast.warning("Please enter a thread title");
return;
}
setLoading(true);
try {
await onCreate({
title: title.trim(),
type,
parentMessageId,
initialMessage: initialMessage.trim() || undefined,
tags: tags.length > 0 ? tags : undefined,
});
toast.success("Thread created");
resetForm();
onClose();
} catch (err: any) {
toast.error(err.message || "Failed to create thread");
} finally {
setLoading(false);
}
};
const addTag = () => {
const t = tagInput.trim().toLowerCase().replace(/[^a-z0-9-]/g, "");
if (t && !tags.includes(t)) {
setTags([...tags, t]);
} else if (tags.includes(t)) {
toast.info("Tag already added");
}
setTagInput("");
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
if (tagInput.trim()) addTag();
else if (title.trim() && initialMessage.trim()) handleCreate();
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" onClick={onClose}>
<div
className="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl dark:bg-gray-800"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-bold">New Thread</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700">
<X className="h-5 w-5" />
</button>
</div>
{/* Parent message preview */}
{parentMessagePreview && (
<div className="mb-4 rounded-xl border border-yellow-200 bg-yellow-50 p-3 text-sm text-gray-600 dark:border-yellow-800 dark:bg-yellow-900/20 dark:text-gray-400">
<p className="mb-1 text-xs font-semibold text-yellow-700 dark:text-yellow-400">
Replying to:
</p>
<p className="line-clamp-2">{parentMessagePreview}</p>
</div>
)}
{/* Title */}
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Thread title (e.g., Discussion of methodology)"
className="mb-4 w-full rounded-xl border border-gray-200 bg-transparent p-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary dark:border-gray-700"
autoFocus
disabled={loading}
/>
{/* Type selector */}
<div className="mb-4 grid grid-cols-3 gap-2">
{THREAD_TYPES.map((t) => (
<button
key={t.value}
onClick={() => setType(t.value)}
disabled={loading}
className={`flex flex-col items-center gap-1 rounded-xl border-2 p-3 text-center transition ${
type === t.value
? "border-primary bg-primary/5 text-primary"
: "border-gray-200 text-gray-500 hover:border-gray-300 dark:border-gray-700"
}`}
title={t.desc}
>
<t.icon className="h-5 w-5" />
<span className="text-xs font-medium">{t.label}</span>
</button>
))}
</div>
{/* Initial message */}
<textarea
value={initialMessage}
onChange={(e) => setInitialMessage(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Start the discussion..."
rows={3}
className="mb-4 w-full resize-none rounded-xl border border-gray-200 bg-transparent p-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary dark:border-gray-700"
disabled={loading}
/>
{/* Tags input */}
<div className="mb-4">
<div className="mb-2 flex gap-2">
<input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addTag())}
placeholder="Add tags (e.g., methodology)"
className="flex-1 rounded-lg border border-gray-200 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-primary dark:border-gray-700"
disabled={loading}
/>
<button
onClick={addTag}
disabled={loading}
className="btn btn-secondary btn-xs"
>
Add
</button>
</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{tags.map((t) => (
<span
key={t}
className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary"
>
<Tag className="h-3 w-3" />
{t}
<button
onClick={() => setTags(tags.filter((x) => x !== t))}
className="ml-0.5 hover:text-red-500"
aria-label={`Remove tag ${t}`}
>
×
</button>
</span>
))}
</div>
)}
</div>
{/* Create button */}
<button
onClick={handleCreate}
disabled={!title.trim() || loading}
className="btn-primary flex w-full items-center justify-center gap-2"
>
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
{loading ? "Creating..." : "Create Thread"}
</button>
</div>
</div>
);
}

Xet Storage Details

Size:
7.46 kB
·
Xet hash:
a279509a072c2f05106236a779d874340e2196a8a450e20e5952cbac61c632c6

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.