"use client"; import { type FormEvent, useEffect, useRef, useState } from "react"; import type { FeedbackCategory } from "@/data/dto/feedback"; import { submitFeedback } from "@/lib/feedback"; import { createFeedbackContext } from "./feedback-context"; import { FEEDBACK_IMAGE_LIMIT, prepareFeedbackImage, } from "./feedback-image"; export const FEEDBACK_CONTENT_MIN_LENGTH = 10; export const FEEDBACK_CONTENT_MAX_LENGTH = 2000; export interface FeedbackImageItem { id: string; file: File; previewUrl: string; } export function useFeedbackSubmission() { const [category, setCategory] = useState("problem"); const [content, setContent] = useState(""); const [images, setImages] = useState([]); const [isPreparingImages, setIsPreparingImages] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [feedbackId, setFeedbackId] = useState(null); const imagesRef = useRef(images); useEffect(() => { imagesRef.current = images; }, [images]); useEffect( () => () => { imagesRef.current.forEach((image) => URL.revokeObjectURL(image.previewUrl)); }, [], ); const addImages = async (files: FileList | readonly File[]) => { if (isPreparingImages || isSubmitting) return; const availableSlots = FEEDBACK_IMAGE_LIMIT - images.length; if (availableSlots <= 0) { setErrorMessage(`You can add up to ${FEEDBACK_IMAGE_LIMIT} images.`); return; } const selectedFiles = Array.from(files); const filesToPrepare = selectedFiles.slice(0, availableSlots); const nextImages: FeedbackImageItem[] = []; const errors: string[] = []; setIsPreparingImages(true); setErrorMessage(null); for (const file of filesToPrepare) { try { const preparedFile = await prepareFeedbackImage(file); nextImages.push({ id: createImageId(), file: preparedFile, previewUrl: URL.createObjectURL(preparedFile), }); } catch (error) { errors.push(error instanceof Error ? error.message : "Image processing failed."); } } if (selectedFiles.length > availableSlots) { errors.push(`You can add up to ${FEEDBACK_IMAGE_LIMIT} images.`); } setImages((current) => [...current, ...nextImages]); setErrorMessage(errors[0] ?? null); setIsPreparingImages(false); }; const removeImage = (id: string) => { if (isSubmitting) return; setImages((current) => { const image = current.find((item) => item.id === id); if (image) URL.revokeObjectURL(image.previewUrl); return current.filter((item) => item.id !== id); }); setErrorMessage(null); }; const submit = async (event: FormEvent) => { event.preventDefault(); if (isSubmitting || isPreparingImages) return; const normalizedContent = content.trim(); const validationError = validateFeedbackContent(normalizedContent); if (validationError) { setErrorMessage(validationError); return; } setIsSubmitting(true); setErrorMessage(null); const result = await submitFeedback({ category, content: normalizedContent, context: createFeedbackContext(), images: images.map((image) => image.file), }); if (result.success) { setFeedbackId(result.data.feedbackId); } else { setErrorMessage(result.error.message || "Feedback could not be sent. Please try again."); } setIsSubmitting(false); }; return { category, content, images, isPreparingImages, isSubmitting, errorMessage, feedbackId, canSubmit: validateFeedbackContent(content.trim()) === null && !isPreparingImages && !isSubmitting, setCategory, setContent, addImages, removeImage, submit, }; } export function validateFeedbackContent(content: string): string | null { if (content.length < FEEDBACK_CONTENT_MIN_LENGTH) { return `Please enter at least ${FEEDBACK_CONTENT_MIN_LENGTH} characters.`; } if (content.length > FEEDBACK_CONTENT_MAX_LENGTH) { return `Feedback cannot exceed ${FEEDBACK_CONTENT_MAX_LENGTH} characters.`; } return null; } function createImageId(): string { return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`; }