import { useState, useEffect, useRef } from "react" import { Send, LogIn, ImagePlus, X } from "lucide-react" import { authClient } from "@/lib/auth-client" import { useAccount, useCoState } from "jazz-tools/react" import { Group, co, FileStream } from "jazz-tools" import { StreamComment, StreamCommentList, StreamCommentsContainer, ViewerAccount, } from "@/lib/jazz/schema" interface CommentBoxProps { username: string } export function CommentBox({ username }: CommentBoxProps) { const { data: session, isPending: sessionLoading } = authClient.useSession() const me = useAccount(ViewerAccount) const [containerId, setContainerId] = useState(undefined) const container = useCoState(StreamCommentsContainer, containerId, { resolve: { comments: true } }) const [newComment, setNewComment] = useState("") const [isSubmitting, setIsSubmitting] = useState(false) const [selectedImage, setSelectedImage] = useState(null) const [imagePreview, setImagePreview] = useState(null) const [uploadProgress, setUploadProgress] = useState(0) // Auth state const [authStep, setAuthStep] = useState<"idle" | "email" | "otp">("idle") const [email, setEmail] = useState("") const [otp, setOtp] = useState("") const [authLoading, setAuthLoading] = useState(false) const [authError, setAuthError] = useState("") const commentsEndRef = useRef(null) const emailInputRef = useRef(null) const otpInputRef = useRef(null) const fileInputRef = useRef(null) // Focus inputs when auth step changes useEffect(() => { if (authStep === "email") { emailInputRef.current?.focus() } else if (authStep === "otp") { otpInputRef.current?.focus() } }, [authStep]) // Initialize or load the comments container for this stream useEffect(() => { if (!me?.$isLoaded) return const initContainer = async () => { try { const containerUID = { stream: username, origin: "linsa.io", type: "comments" } // Create a group writable by everyone const group = Group.create({ owner: me }) group.addMember("everyone", "writer") // Upsert the container const result = await StreamCommentsContainer.upsertUnique({ value: { comments: StreamCommentList.create([], { owner: group }) }, unique: containerUID, owner: group, }) setContainerId(result.$jazz.id) } catch (err) { console.error("Failed to init comments container:", err) } } initContainer() }, [me?.$isLoaded, username]) // Get comments from the container (only when loaded) const comments = container?.$isLoaded ? container.comments : undefined // Scroll to bottom when new comments arrive useEffect(() => { commentsEndRef.current?.scrollIntoView({ behavior: "smooth" }) }, [comments?.length]) const handleImageSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return // Validate file type if (!file.type.startsWith("image/")) { alert("Please select an image file") return } // Validate file size (max 10MB) if (file.size > 10 * 1024 * 1024) { alert("Image must be less than 10MB") return } setSelectedImage(file) setImagePreview(URL.createObjectURL(file)) } const clearSelectedImage = () => { setSelectedImage(null) if (imagePreview) { URL.revokeObjectURL(imagePreview) setImagePreview(null) } if (fileInputRef.current) { fileInputRef.current.value = "" } } const handleSendOTP = async (e: React.FormEvent) => { e.preventDefault() if (!email.trim()) return setAuthLoading(true) setAuthError("") try { const result = await authClient.emailOtp.sendVerificationOtp({ email, type: "sign-in", }) if (result.error) { setAuthError(result.error.message || "Failed to send code") } else { setAuthStep("otp") } } catch (err) { setAuthError(err instanceof Error ? err.message : "Failed to send verification code") } finally { setAuthLoading(false) } } const handleVerifyOTP = async (e: React.FormEvent) => { e.preventDefault() if (!otp.trim()) return setAuthLoading(true) setAuthError("") try { const result = await authClient.signIn.emailOtp({ email, otp, }) if (result.error) { setAuthError(result.error.message || "Invalid code") } else { setAuthStep("idle") setEmail("") setOtp("") } } catch (err) { setAuthError(err instanceof Error ? err.message : "Failed to verify code") } finally { setAuthLoading(false) } } const handleSubmitComment = async (e: React.FormEvent) => { e.preventDefault() const commentsList = container?.$isLoaded ? container.comments : undefined if ((!newComment.trim() && !selectedImage) || !session?.user || !me?.$isLoaded || !commentsList?.$isLoaded) return setIsSubmitting(true) setUploadProgress(0) try { // Create a group for the comment const group = Group.create({ owner: me }) group.addMember("everyone", "reader") // Upload image if selected let imageStream = undefined if (selectedImage) { imageStream = await co.fileStream().createFromBlob(selectedImage, { owner: group, onProgress: (progress) => { setUploadProgress(Math.round(progress * 100)) }, }) } // Create the comment const comment = StreamComment.create( { content: newComment.trim(), userName: session.user.name || session.user.email?.split("@")[0] || "Anonymous", userId: session.user.id || null, image: imageStream, createdAt: Date.now(), }, { owner: group } ) // Add to list ;(commentsList as unknown as { push: (item: typeof comment) => void }).push(comment) // Clear form setNewComment("") clearSelectedImage() setUploadProgress(0) } catch (err) { console.error("Failed to post comment:", err) } finally { setIsSubmitting(false) } } const formatTime = (timestamp: number) => { const date = new Date(timestamp) return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) } const isAuthenticated = !!session?.user return (
{/* Header */}

Chat

{/* Comments list */}
{!comments ? (
Loading...
) : comments.length === 0 ? (
No messages yet. Be the first to say hi!
) : ( comments.map((comment, index: number) => { if (!comment?.$isLoaded) return null return (
{comment.userName?.charAt(0).toUpperCase() || "?"}
{comment.userName || "Anonymous"} {formatTime(comment.createdAt)}
{comment.content && (

{comment.content}

)} {comment.image?.$isLoaded && }
) }) )}
{/* Input area */}
{sessionLoading ? (
Loading...
) : isAuthenticated ? (
{/* Image preview */} {imagePreview && (
Preview
)} {/* Upload progress */} {uploadProgress > 0 && uploadProgress < 100 && (
)}
setNewComment(e.target.value)} placeholder="Send a message..." className="flex-1 bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-white/20" disabled={isSubmitting} />
) : authStep === "idle" ? ( ) : authStep === "email" ? (
setEmail(e.target.value)} className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-white/20" /> {authError &&

{authError}

}
) : (

Code sent to {email}

setOtp(e.target.value.replace(/\D/g, ""))} className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-center text-lg font-mono tracking-widest text-white placeholder:text-white/30 focus:outline-none focus:border-white/20" /> {authError &&

{authError}

}
)}
) } // Component to display an image from a FileStream function CommentImage({ image }: { image: FileStream }) { const [url, setUrl] = useState(null) useEffect(() => { if (!image) return try { const blob = image.toBlob() if (blob) { const objectUrl = URL.createObjectURL(blob) setUrl(objectUrl) return () => URL.revokeObjectURL(objectUrl) } } catch { // Image still loading } }, [image]) if (!url) { return (
Loading...
) } return ( Attached window.open(url, "_blank")} /> ) }