153 lines
4.4 KiB
TypeScript
153 lines
4.4 KiB
TypeScript
"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<FeedbackCategory>("problem");
|
|
const [content, setContent] = useState("");
|
|
const [images, setImages] = useState<FeedbackImageItem[]>([]);
|
|
const [isPreparingImages, setIsPreparingImages] = useState(false);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
const [feedbackId, setFeedbackId] = useState<string | null>(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<HTMLFormElement>) => {
|
|
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)}`;
|
|
}
|