PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / resources / js / pages / ViewReview.tsx

ViewReview.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.10, at resources/js/pages/ViewReview.tsx

457 lines 16.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * View Review Page
3 * Display review details in a clean, minimal SaaS-style design
4 */
5
6 import React, { useMemo } from "react";
7 import { useQuery } from "@tanstack/react-query";
8 import { ArrowLeft, Star, Mail, Calendar, Edit, MapPin } from "lucide-react";
9 import { __ } from "../lib/i18n";
10 import { usePermissions } from "../hooks/usePermissions";
11 import { apiClient } from "../lib/api-client";
12 import { Button } from "../components/ui/button";
13 import { PageHeader } from "../components/common/PageHeader";
14 import {
15 Card,
16 CardContent,
17 CardHeader,
18 CardTitle,
19 } from "../components/ui/card";
20 import { ConditionalRender } from "../components/ui/conditional-render";
21
22 interface ReviewData {
23 id: number;
24 trip_id: number;
25 trip_title: string;
26 trip_slug?: string;
27 customer_name: string;
28 customer_email: string;
29 customer_location?: string;
30 rating: number;
31 title: string;
32 content: string;
33 status: string;
34 verified: boolean;
35 created_at: string;
36 updated_at?: string;
37 }
38
39 const ViewReview: React.FC = () => {
40 const { can } = usePermissions();
41
42 // Get review id from URL
43 const reviewId = useMemo(() => {
44 const params = new URLSearchParams(window.location.search);
45 return params.get("id") ? parseInt(params.get("id") || "0") : null;
46 }, []);
47
48 // Fetch review data from API
49 const {
50 data: review,
51 isLoading,
52 error,
53 } = useQuery<ReviewData | null>({
54 queryKey: ["review", reviewId],
55 queryFn: async () => {
56 if (!reviewId) return null;
57 const response = await apiClient.get(`/reviews/${reviewId}`);
58 // Transform API response
59 const data = response?.data || response;
60 return {
61 id: data.id,
62 trip_id: data.trip_id,
63 trip_title: data.trip_title || "Unknown Trip",
64 trip_slug: data.trip_slug || "",
65 customer_name: data.customer_name || data.author_name || "Anonymous",
66 customer_email: data.customer_email || data.author_email || "",
67 customer_location: data.customer_location || data.author_location || "",
68 rating: data.rating,
69 title: data.title || "",
70 content: data.content || "",
71 status: data.status || "pending",
72 verified: data.verified || false,
73 created_at: data.created_at || "",
74 updated_at: data.updated_at || "",
75 };
76 },
77 enabled: !!reviewId && can("yatra_view_reviews"),
78 });
79
80 const formatDate = (dateString: string) => {
81 return new Date(dateString).toLocaleDateString("en-US", {
82 weekday: "long",
83 year: "numeric",
84 month: "long",
85 day: "numeric",
86 });
87 };
88
89 const renderStars = (rating: number) => {
90 return (
91 <div className="flex items-center gap-1">
92 {[1, 2, 3, 4, 5].map((star) => (
93 <Star
94 key={star}
95 className={`w-6 h-6 ${
96 star <= rating
97 ? "fill-yellow-400 text-yellow-400"
98 : "fill-gray-300 text-gray-300 dark:fill-gray-600 dark:text-gray-600"
99 }`}
100 />
101 ))}
102 <span className="ml-2 text-lg font-semibold text-gray-900 dark:text-white">
103 {rating} {__("out of 5", "yatra")}
104 </span>
105 </div>
106 );
107 };
108
109 const getStatusBadge = (status: string) => {
110 const statusMap: Record<string, { className: string; label: string }> = {
111 approved: {
112 className:
113 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
114 label: __("Approved", "yatra"),
115 },
116 pending: {
117 className:
118 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
119 label: __("Pending", "yatra"),
120 },
121 spam: {
122 className:
123 "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400",
124 label: __("Spam", "yatra"),
125 },
126 trash: {
127 className:
128 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
129 label: __("Trash", "yatra"),
130 },
131 };
132
133 const statusInfo = statusMap[status] || {
134 className:
135 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
136 label: status,
137 };
138
139 return (
140 <span
141 className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${statusInfo.className}`}
142 >
143 {statusInfo.label}
144 </span>
145 );
146 };
147
148 const handleBack = () => {
149 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reviews`;
150 };
151
152 const handleEdit = () => {
153 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reviews&action=edit&id=${reviewId}`;
154 };
155
156 if (isLoading) {
157 return (
158 <div className="space-y-3">
159 <PageHeader
160 title={__("Review Details", "yatra")}
161 description={__("View complete review information", "yatra")}
162 actions={
163 <Button
164 variant="outline"
165 onClick={handleBack}
166 className="flex items-center gap-2"
167 >
168 <ArrowLeft className="w-4 h-4" />
169 {__("Back", "yatra")}
170 </Button>
171 }
172 />
173 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
174 {/* Main Content Skeleton */}
175 <div className="lg:col-span-2 space-y-3">
176 <Card>
177 <CardHeader className="pb-2">
178 <div className="flex items-center justify-between">
179 <div className="h-5 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
180 <div className="h-6 w-20 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse" />
181 </div>
182 </CardHeader>
183 <CardContent className="space-y-4">
184 <div>
185 <div className="h-3 w-12 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2" />
186 <div className="h-6 w-48 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
187 </div>
188 <div>
189 <div className="h-3 w-12 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2" />
190 <div className="flex gap-1">
191 {[...Array(5)].map((_, i) => (
192 <div
193 key={i}
194 className="h-6 w-6 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"
195 />
196 ))}
197 </div>
198 </div>
199 <div>
200 <div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2" />
201 <div className="h-6 w-64 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
202 </div>
203 <div>
204 <div className="h-3 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2" />
205 <div className="space-y-2">
206 <div className="h-4 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
207 <div className="h-4 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
208 <div className="h-4 w-3/4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
209 </div>
210 </div>
211 </CardContent>
212 </Card>
213 </div>
214 {/* Sidebar Skeleton */}
215 <div className="space-y-3">
216 <Card>
217 <CardHeader className="pb-2">
218 <div className="h-5 w-40 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
219 </CardHeader>
220 <CardContent className="space-y-3">
221 <div>
222 <div className="h-3 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-1" />
223 <div className="h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
224 </div>
225 <div>
226 <div className="h-3 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-1" />
227 <div className="h-4 w-40 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
228 </div>
229 </CardContent>
230 </Card>
231 <Card>
232 <CardHeader className="pb-2">
233 <div className="h-5 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
234 </CardHeader>
235 <CardContent className="space-y-3">
236 <div>
237 <div className="h-3 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-1" />
238 <div className="h-4 w-36 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
239 </div>
240 </CardContent>
241 </Card>
242 </div>
243 </div>
244 </div>
245 );
246 }
247
248 if (error || !review) {
249 return (
250 <div className="space-y-3">
251 <PageHeader
252 title={__("Review Not Found", "yatra")}
253 description={__(
254 "The review you are looking for does not exist",
255 "yatra",
256 )}
257 actions={
258 <Button
259 variant="outline"
260 onClick={handleBack}
261 className="flex items-center gap-2"
262 >
263 <ArrowLeft className="w-4 h-4" />
264 {__("Back to Reviews", "yatra")}
265 </Button>
266 }
267 />
268 <Card>
269 <CardContent className="p-8 text-center text-red-500">
270 {__("Error loading review or review not found", "yatra")}
271 </CardContent>
272 </Card>
273 </div>
274 );
275 }
276
277 return (
278 <div className="space-y-3">
279 <PageHeader
280 title={__("Review Details", "yatra")}
281 description={__("View complete review information", "yatra")}
282 actions={
283 <div className="flex gap-2">
284 <ConditionalRender capability="yatra_edit_reviews">
285 <Button onClick={handleEdit} className="flex items-center gap-2">
286 <Edit className="w-4 h-4" />
287 {__("Edit Review", "yatra")}
288 </Button>
289 </ConditionalRender>
290 <Button
291 variant="outline"
292 onClick={handleBack}
293 className="flex items-center gap-2"
294 >
295 <ArrowLeft className="w-4 h-4" />
296 {__("Back", "yatra")}
297 </Button>
298 </div>
299 }
300 />
301
302 <ConditionalRender capability="yatra_view_reviews">
303 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
304 {/* Main Content */}
305 <div className="lg:col-span-2 space-y-3">
306 {/* Review Overview */}
307 <Card>
308 <CardHeader className="pb-2">
309 <div className="flex items-center justify-between">
310 <CardTitle className="text-base">
311 {__("Review Overview", "yatra")}
312 </CardTitle>
313 {getStatusBadge(review.status)}
314 </div>
315 </CardHeader>
316 <CardContent className="space-y-4">
317 {/* Trip */}
318 <div>
319 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
320 {__("Trip", "yatra")}
321 </div>
322 {review.trip_id ? (
323 <a
324 href={`${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&action=edit&id=${review.trip_id}`}
325 className="text-lg font-semibold text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
326 >
327 {review.trip_title}
328 </a>
329 ) : (
330 <div className="text-lg font-semibold text-gray-900 dark:text-white">
331 {review.trip_title}
332 </div>
333 )}
334 </div>
335
336 {/* Rating */}
337 <div>
338 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">
339 {__("Rating", "yatra")}
340 </div>
341 {renderStars(review.rating)}
342 </div>
343
344 {/* Title */}
345 <div>
346 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
347 {__("Review Title", "yatra")}
348 </div>
349 <div className="text-lg font-semibold text-gray-900 dark:text-white">
350 {review.title}
351 </div>
352 </div>
353
354 {/* Comment */}
355 <div>
356 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">
357 {__("Review Comment", "yatra")}
358 </div>
359 <div className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap leading-relaxed">
360 {review.content}
361 </div>
362 </div>
363 </CardContent>
364 </Card>
365 </div>
366
367 {/* Sidebar */}
368 <div className="space-y-3">
369 {/* Customer Information */}
370 <Card>
371 <CardHeader className="pb-2">
372 <CardTitle className="text-base">
373 {__("Customer Information", "yatra")}
374 </CardTitle>
375 </CardHeader>
376 <CardContent className="space-y-3">
377 <div>
378 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
379 {__("Customer Name", "yatra")}
380 </div>
381 <div className="text-sm font-medium text-gray-900 dark:text-white">
382 {review.customer_name}
383 </div>
384 </div>
385 <div>
386 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
387 <Mail className="w-3 h-3" />
388 {__("Email Address", "yatra")}
389 </div>
390 <div className="text-sm text-gray-900 dark:text-white">
391 {review.customer_email || (
392 <span className="text-gray-400">
393 {__("Not provided", "yatra")}
394 </span>
395 )}
396 </div>
397 </div>
398 {review.customer_location && (
399 <div>
400 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
401 <MapPin className="w-3 h-3" />
402 {__("Location", "yatra")}
403 </div>
404 <div className="text-sm text-gray-900 dark:text-white">
405 {review.customer_location}
406 </div>
407 </div>
408 )}
409 {review.verified && (
410 <div>
411 <span className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400">
412 {__("Verified Purchase", "yatra")}
413 </span>
414 </div>
415 )}
416 </CardContent>
417 </Card>
418
419 {/* Timeline */}
420 <Card>
421 <CardHeader className="pb-2">
422 <CardTitle className="text-base">
423 {__("Timeline", "yatra")}
424 </CardTitle>
425 </CardHeader>
426 <CardContent className="space-y-3">
427 <div>
428 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
429 <Calendar className="w-3 h-3" />
430 {__("Created", "yatra")}
431 </div>
432 <div className="text-sm text-gray-900 dark:text-white">
433 {formatDate(review.created_at)}
434 </div>
435 </div>
436 {review.updated_at &&
437 review.updated_at !== review.created_at && (
438 <div>
439 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
440 {__("Last Updated", "yatra")}
441 </div>
442 <div className="text-sm text-gray-900 dark:text-white">
443 {formatDate(review.updated_at)}
444 </div>
445 </div>
446 )}
447 </CardContent>
448 </Card>
449 </div>
450 </div>
451 </ConditionalRender>
452 </div>
453 );
454 };
455
456 export default ViewReview;
457