Files
frontend/components/organisms/modals/ReviewModal.tsx
T
TakahashiNguyen 2376d57cbb
Release package / release (push) Successful in 8m30s
fix: final commit (#46)
Co-authored-by: TakahashiNg <83152264+TakahashiNguyen@users.noreply.github.com>
Reviewed-on: #46
2026-05-15 13:22:12 +00:00

202 lines
6.1 KiB
TypeScript

"use client";
import { Button, Heading, Text, Textarea } from "@/components/atoms";
import { useAuth } from "@/lib/auth-context";
import { useState } from "react";
import type { ReviewModalProps } from "./Modal.types";
interface CreateReviewInput {
reviewerId: string;
eateryId: string;
rating: number;
comment?: string;
}
export default function ReviewModal({
isOpen,
onClose,
eateryId,
}: ReviewModalProps) {
const { user } = useAuth();
const [rating, setRating] = useState(0);
const [hovered, setHovered] = useState(0);
const [review, setReview] = useState("");
const [submitted, setSubmitted] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
if (!isOpen) return null;
const handleSubmit = async () => {
if (!eateryId) return;
const input: CreateReviewInput = {
reviewerId: user?.id || "",
eateryId,
rating,
...(review.trim() ? { comment: review.trim() } : {}),
};
setLoading(true);
setError(null);
try {
const res = await fetch("/api/eatery/review", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
setSubmitted(true);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to submit review.");
} finally {
setLoading(false);
}
};
const handleClose = () => {
setRating(0);
setHovered(0);
setReview("");
setSubmitted(false);
setError(null);
onClose();
};
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
aria-labelledby="review-modal-title"
>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={handleClose}
aria-hidden="true"
/>
{/* Modal */}
<div className="relative w-full max-w-md rounded-2xl border border-(--color-border-light) bg-white p-6 shadow-xl sm:p-8">
{submitted ? (
/* Thank you state */
<div className="flex flex-col items-center gap-4 py-4 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-(--color-accent-light) text-3xl">
<i className="fa-solid fa-heart text-(--color-accent)"></i>
</div>
<Heading level={2} id="review-modal-title">
Thank you
</Heading>
<Text variant="body2" className="mt-2">
We appreciate your feedback!
</Text>
<Button onClick={handleClose} variant="primary" className="mt-4">
Close
</Button>
</div>
) : (
/* Review form */
<>
<h2
id="review-modal-title"
className="text-foreground mb-1 text-xl font-bold"
>
Your Review
</h2>
<p className="mb-5 text-sm text-(--color-text-muted)">
Tell us about your experience today
</p>
{/* Star rating */}
<div className="mb-5">
<p className="mb-2 text-sm font-medium text-(--color-text-secondary)">
Satisfaction level
</p>
<div
className="flex gap-2"
role="radiogroup"
aria-label="Star rating"
>
{[1, 2, 3, 4, 5].map((star) => {
const isActive = star <= (hovered || rating);
return (
<button
key={star}
type="button"
onClick={() => setRating(star)}
onMouseEnter={() => setHovered(star)}
onMouseLeave={() => setHovered(0)}
aria-label={`${star} star`}
aria-pressed={rating === star}
className="text-3xl transition-transform hover:scale-110 active:scale-95 sm:text-4xl"
>
<i
className={
isActive
? "fa-solid fa-star text-yellow-400"
: "fa-regular fa-star text-(--color-border)"
}
></i>
</button>
);
})}
</div>
{rating > 0 && (
<p className="mt-1.5 text-xs text-(--color-text-muted)">
{
["", "Very poor", "Poor", "Average", "Good", "Excellent"][
rating
]
}
</p>
)}
</div>
{/* Review textarea */}
<div className="mb-6">
<Textarea
id="review-text"
label="Comment (optional)"
value={review}
onChange={(e) => setReview(e.target.value)}
placeholder="Share your thoughts on the drinks, service..."
rows={4}
/>
</div>
{/* Error message */}
{error && <p className="mb-4 text-sm text-red-500">{error}</p>}
{/* Footer buttons */}
<div className="flex gap-3">
<Button
type="button"
onClick={handleClose}
variant="secondary"
className="flex-1"
icon="fa-arrow-left"
disabled={loading}
>
Go back
</Button>
<Button
type="button"
onClick={handleSubmit}
disabled={rating === 0 || loading}
className="flex-1"
icon={loading ? "fa-spinner fa-spin" : "fa-check"}
>
{loading ? "Submitting..." : "Confirm"}
</Button>
</div>
</>
)}
</div>
</div>
);
}