This commit is contained in:
Iliyan Angelov
2025-11-28 14:36:37 +02:00
parent 312f85530c
commit b5698b6018
12 changed files with 191 additions and 35 deletions

View File

@@ -7,7 +7,7 @@
<!-- Content Security Policy - Additional layer of XSS protection -->
<!-- Allows HTTP localhost connections for development, HTTPS for production -->
<!-- Note: Backend CSP headers (production only) will override/merge with this meta tag -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https: http: blob:; connect-src 'self' https: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* wss:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https: http: blob:; connect-src 'self' https: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* wss: https://js.stripe.com https://hooks.stripe.com; frame-src 'self' https://js.stripe.com https://hooks.stripe.com; object-src 'none'; base-uri 'self'; form-action 'self';" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700;800;900&family=Cormorant+Garamond:wght@300;400;500;600;700&family=Cinzel:wght@400;500;600;700&family=Poppins:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />

View File

@@ -86,6 +86,7 @@ const RatePlanManagementPage = lazy(() => import('./pages/admin/RatePlanManageme
const PackageManagementPage = lazy(() => import('./pages/admin/PackageManagementPage'));
const SecurityManagementPage = lazy(() => import('./pages/admin/SecurityManagementPage'));
const EmailCampaignManagementPage = lazy(() => import('./pages/admin/EmailCampaignManagementPage'));
const ReviewManagementPage = lazy(() => import('./pages/admin/ReviewManagementPage'));
const StaffDashboardPage = lazy(() => import('./pages/staff/DashboardPage'));
const StaffBookingManagementPage = lazy(() => import('./pages/staff/BookingManagementPage'));
@@ -464,6 +465,10 @@ function App() {
path="email-campaigns"
element={<EmailCampaignManagementPage />}
/>
<Route
path="reviews"
element={<ReviewManagementPage />}
/>
</Route>
{}

View File

@@ -26,7 +26,8 @@ import {
Mail,
TrendingUp,
Building2,
Crown
Crown,
Star
} from 'lucide-react';
import useAuthStore from '../../store/useAuthStore';
import { useResponsive } from '../../hooks';
@@ -212,6 +213,11 @@ const SidebarAdmin: React.FC<SidebarAdminProps> = ({
icon: Globe,
label: 'Page Content'
},
{
path: '/admin/reviews',
icon: Star,
label: 'Reviews'
},
]
},
{

View File

@@ -74,9 +74,22 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
setLoading(true);
const response = await getRoomReviews(roomId);
if (response.status === 'success' && response.data) {
setReviews(response.data.reviews || []);
setAverageRating(response.data.average_rating || 0);
setTotalReviews(response.data.total_reviews || 0);
const reviewsList = response.data.reviews || [];
setReviews(reviewsList);
// Use backend values, but fallback to calculated values if backend doesn't provide them
const backendTotal = response.data.total_reviews || 0;
const backendAverage = response.data.average_rating || 0;
// If backend doesn't provide totals but we have reviews, calculate them
if (backendTotal === 0 && reviewsList.length > 0) {
const calculatedTotal = reviewsList.length;
const calculatedAverage = reviewsList.reduce((sum, r) => sum + r.rating, 0) / calculatedTotal;
setTotalReviews(calculatedTotal);
setAverageRating(calculatedAverage);
} else {
setTotalReviews(backendTotal);
setAverageRating(backendAverage);
}
}
} catch (error) {
console.error('Error fetching reviews:', error);
@@ -153,9 +166,11 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
<div className="flex items-center gap-4">
<div className="text-center">
<div className="text-2xl sm:text-3xl font-serif font-bold bg-gradient-to-r from-[#d4af37] to-[#f5d76e] bg-clip-text text-transparent">
{averageRating > 0
{totalReviews > 0 && averageRating > 0
? averageRating.toFixed(1)
: 'N/A'}
: totalReviews === 0
? '—'
: averageRating.toFixed(1)}
</div>
<div className="mt-1">
<RatingStars
@@ -164,7 +179,9 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
/>
</div>
<div className="text-[10px] sm:text-xs text-gray-400 mt-1.5 font-light">
{totalReviews} review{totalReviews !== 1 ? 's' : ''}
{totalReviews > 0
? `${totalReviews} review${totalReviews !== 1 ? 's' : ''}`
: 'No reviews yet'}
</div>
</div>
</div>
@@ -274,7 +291,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
{}
<div>
<h4 className="text-xs sm:text-sm font-serif font-semibold text-white mb-3 tracking-wide">
All Reviews ({totalReviews})
All Reviews ({reviews.length > 0 ? reviews.length : totalReviews})
</h4>
{loading ? (

View File

@@ -146,7 +146,12 @@ const ReviewManagementPage: React.FC = () => {
{reviews.map((review) => (
<tr key={review.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{review.user?.name}</div>
<div className="text-sm font-medium text-gray-900">
{review.user?.full_name || review.user?.name || 'Unknown User'}
</div>
{review.user?.email && (
<div className="text-xs text-gray-500">{review.user.email}</div>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">