202 lines
6.1 KiB
TypeScript
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 (!user?.id || !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>
|
|
);
|
|
}
|