first commit
This commit is contained in:
143
app/apps/[id]/store/ReviewCard.tsx
Normal file
143
app/apps/[id]/store/ReviewCard.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { Star, MessageSquare, Loader2, CheckCircle2, ChevronDown, ChevronUp } from "lucide-react";
|
||||
|
||||
function StarRating({ rating }: { rating: number }) {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
{[1, 2, 3, 4, 5].map((s) => (
|
||||
<Star
|
||||
key={s}
|
||||
size={10}
|
||||
className={s <= rating ? "fill-yellow-400 text-yellow-400" : "text-slate-700"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TERRITORY_NAMES: Record<string, string> = {
|
||||
USA: "🇺🇸", TUR: "🇹🇷", GBR: "🇬🇧", DEU: "🇩🇪", FRA: "🇫🇷",
|
||||
JPN: "🇯🇵", KOR: "🇰🇷", CHN: "🇨🇳", AUS: "🇦🇺", CAN: "🇨🇦",
|
||||
BRA: "🇧🇷", IND: "🇮🇳", RUS: "🇷🇺", ESP: "🇪🇸", ITA: "🇮🇹",
|
||||
};
|
||||
|
||||
export default function ReviewCard({ review }: { review: any }) {
|
||||
const attr = review.attributes ?? {};
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [showReply, setShowReply] = useState(false);
|
||||
const [replyText, setReplyText] = useState("");
|
||||
const [submitting, startTransition] = useTransition();
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const bodyPreview = attr.body?.length > 140 ? attr.body.slice(0, 140) + "…" : attr.body;
|
||||
|
||||
function handleReply() {
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
const res = await fetch("/api/asc/review-response", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ reviewId: review.id, responseBody: replyText }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSubmitted(true);
|
||||
setShowReply(false);
|
||||
setTimeout(() => setSubmitted(false), 3000);
|
||||
} else {
|
||||
const d = await res.json();
|
||||
setError(d.error ?? "Gönderilemedi");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-5 py-4 hover:bg-white/[0.02] transition-colors">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<StarRating rating={attr.rating ?? 0} />
|
||||
<span className="text-[10px] text-slate-500">
|
||||
{TERRITORY_NAMES[attr.territory] ?? ""} {attr.territory}
|
||||
</span>
|
||||
</div>
|
||||
{attr.title && (
|
||||
<p className="text-xs font-semibold text-white truncate">{attr.title}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600 shrink-0">
|
||||
{attr.createdDate
|
||||
? new Date(attr.createdDate).toLocaleDateString("tr-TR", {
|
||||
month: "short", day: "numeric", year: "numeric",
|
||||
})
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="mt-2">
|
||||
<p className="text-[11px] text-slate-400 leading-relaxed">
|
||||
{expanded ? attr.body : bodyPreview}
|
||||
</p>
|
||||
{attr.body?.length > 140 && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="mt-1 flex items-center gap-0.5 text-[10px] text-slate-500 hover:text-slate-300 transition-colors"
|
||||
>
|
||||
{expanded ? <><ChevronUp size={10} /> Daralt</> : <><ChevronDown size={10} /> Devamını gör</>}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reviewer */}
|
||||
{attr.reviewerNickname && (
|
||||
<p className="mt-1.5 text-[10px] text-slate-600 italic">— {attr.reviewerNickname}</p>
|
||||
)}
|
||||
|
||||
{/* Reply section */}
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
{submitted ? (
|
||||
<span className="flex items-center gap-1 text-[10px] text-emerald-400">
|
||||
<CheckCircle2 size={10} /> Yanıt gönderildi
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowReply(!showReply)}
|
||||
className="flex items-center gap-1 text-[10px] font-semibold text-violet-400 hover:text-violet-300 transition-colors"
|
||||
>
|
||||
<MessageSquare size={10} />
|
||||
{showReply ? "İptal" : "Yanıtla"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showReply && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<textarea
|
||||
value={replyText}
|
||||
onChange={(e) => setReplyText(e.target.value)}
|
||||
placeholder="Kullanıcıya yanıtınızı yazın…"
|
||||
rows={3}
|
||||
maxLength={5900}
|
||||
className="w-full bg-black/30 border border-white/8 rounded-xl px-3 py-2 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-violet-500/50 resize-none"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
{error && <p className="text-[10px] text-red-400">{error}</p>}
|
||||
<span className="text-[10px] text-slate-600 ml-auto">{replyText.length}/5900</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleReply}
|
||||
disabled={submitting || !replyText.trim()}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-violet-600 hover:bg-violet-500 disabled:opacity-40 text-[11px] font-bold transition-colors"
|
||||
>
|
||||
{submitting ? <Loader2 size={10} className="animate-spin" /> : <MessageSquare size={10} />}
|
||||
Gönder
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user