updates
This commit is contained in:
@@ -93,6 +93,7 @@ const NotificationManagementPage = lazy(() => import('./pages/admin/Notification
|
||||
const ReceptionDashboardPage = lazy(() => import('./pages/admin/ReceptionDashboardPage'));
|
||||
const LoyaltyManagementPage = lazy(() => import('./pages/admin/LoyaltyManagementPage'));
|
||||
const AdvancedRoomManagementPage = lazy(() => import('./pages/admin/AdvancedRoomManagementPage'));
|
||||
const EditRoomPage = lazy(() => import('./pages/admin/EditRoomPage'));
|
||||
const RatePlanManagementPage = lazy(() => import('./pages/admin/RatePlanManagementPage'));
|
||||
const PackageManagementPage = lazy(() => import('./pages/admin/PackageManagementPage'));
|
||||
const SecurityManagementPage = lazy(() => import('./pages/admin/SecurityManagementPage'));
|
||||
@@ -543,6 +544,10 @@ function App() {
|
||||
path="advanced-rooms"
|
||||
element={<AdvancedRoomManagementPage />}
|
||||
/>
|
||||
<Route
|
||||
path="rooms/:id/edit"
|
||||
element={<EditRoomPage />}
|
||||
/>
|
||||
<Route
|
||||
path="page-content"
|
||||
element={<PageContentDashboardPage />}
|
||||
|
||||
@@ -143,6 +143,26 @@ class BlogService {
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Tag management endpoints
|
||||
async getAllTags(): Promise<{ status: string; data: { tags: Array<{ name: string; usage_count: number }>; total: number } }> {
|
||||
const response = await apiClient.get('/blog/admin/tags');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async renameTag(oldTag: string, newTag: string): Promise<{ status: string; data: { old_tag: string; new_tag: string; updated_posts: number }; message?: string }> {
|
||||
const response = await apiClient.put('/blog/admin/tags/rename', null, {
|
||||
params: { old_tag: oldTag, new_tag: newTag },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteTag(tag: string): Promise<{ status: string; data: { deleted_tag: string; updated_posts: number }; message?: string }> {
|
||||
const response = await apiClient.delete('/blog/admin/tags', {
|
||||
params: { tag },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const blogService = new BlogService();
|
||||
|
||||
@@ -169,6 +169,33 @@ export const getAmenities = async (): Promise<{
|
||||
};
|
||||
};
|
||||
|
||||
export const updateAmenity = async (
|
||||
oldName: string,
|
||||
newName: string
|
||||
): Promise<{ success: boolean; message: string; data: { updated_count: number; old_name: string; new_name: string } }> => {
|
||||
const response = await apiClient.put(`/rooms/amenities/${encodeURIComponent(oldName)}`, {
|
||||
new_name: newName,
|
||||
});
|
||||
const responseData = response.data;
|
||||
return {
|
||||
success: responseData.status === 'success' || responseData.success === true,
|
||||
message: responseData.message || '',
|
||||
data: responseData.data || { updated_count: 0, old_name: oldName, new_name: newName },
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteAmenity = async (
|
||||
amenityName: string
|
||||
): Promise<{ success: boolean; message: string; data: { updated_count: number; amenity_name: string } }> => {
|
||||
const response = await apiClient.delete(`/rooms/amenities/${encodeURIComponent(amenityName)}`);
|
||||
const data = response.data;
|
||||
return {
|
||||
success: data.status === 'success' || data.success === true,
|
||||
message: data.message || '',
|
||||
data: data.data || { updated_count: 0, amenity_name: amenityName },
|
||||
};
|
||||
};
|
||||
|
||||
export interface RoomType {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -264,6 +291,8 @@ export default {
|
||||
getRoomByNumber,
|
||||
searchAvailableRooms,
|
||||
getAmenities,
|
||||
updateAmenity,
|
||||
deleteAmenity,
|
||||
getRoomTypes,
|
||||
createRoom,
|
||||
updateRoom,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Hotel,
|
||||
Wrench,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
MapPin,
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
X,
|
||||
Image as ImageIcon,
|
||||
Check,
|
||||
@@ -35,6 +37,7 @@ import { useRoomContext } from '../../features/rooms/contexts/RoomContext';
|
||||
type Tab = 'status-board' | 'maintenance' | 'housekeeping' | 'inspections';
|
||||
|
||||
const AdvancedRoomManagementPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
statusBoardRooms,
|
||||
statusBoardLoading,
|
||||
@@ -73,6 +76,8 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
const [roomTypes, setRoomTypes] = useState<Array<{ id: number; name: string }>>([]);
|
||||
const [uploadingImages, setUploadingImages] = useState(false);
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [customAmenityInput, setCustomAmenityInput] = useState('');
|
||||
const [editingAmenity, setEditingAmenity] = useState<{ name: string; newName: string } | null>(null);
|
||||
|
||||
// Define fetchFloors before using it in useEffect
|
||||
const fetchFloors = useCallback(async () => {
|
||||
@@ -451,87 +456,31 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditRoom = async (room: Room) => {
|
||||
setEditingRoom(room);
|
||||
|
||||
let amenitiesArray: string[] = [];
|
||||
if (room.amenities) {
|
||||
if (Array.isArray(room.amenities)) {
|
||||
amenitiesArray = room.amenities;
|
||||
} else if (typeof room.amenities === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(room.amenities);
|
||||
amenitiesArray = Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
const amenitiesStr: string = room.amenities;
|
||||
amenitiesArray = amenitiesStr.split(',').map((a: string) => a.trim()).filter(Boolean);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setRoomFormData({
|
||||
room_number: room.room_number,
|
||||
floor: room.floor,
|
||||
room_type_id: room.room_type_id,
|
||||
status: room.status,
|
||||
featured: room.featured,
|
||||
price: room.price?.toString() || '',
|
||||
description: room.description || '',
|
||||
capacity: room.capacity?.toString() || '',
|
||||
room_size: room.room_size || '',
|
||||
view: room.view || '',
|
||||
amenities: amenitiesArray,
|
||||
});
|
||||
|
||||
setShowRoomModal(true);
|
||||
|
||||
try {
|
||||
const fullRoom = await roomService.getRoomByNumber(room.room_number);
|
||||
const roomData = fullRoom.data.room;
|
||||
|
||||
let updatedAmenitiesArray: string[] = [];
|
||||
if (roomData.amenities) {
|
||||
if (Array.isArray(roomData.amenities)) {
|
||||
updatedAmenitiesArray = roomData.amenities;
|
||||
} else if (typeof roomData.amenities === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(roomData.amenities);
|
||||
updatedAmenitiesArray = Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
const amenitiesStr: string = roomData.amenities;
|
||||
updatedAmenitiesArray = amenitiesStr.split(',').map((a: string) => a.trim()).filter(Boolean);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setRoomFormData({
|
||||
room_number: roomData.room_number,
|
||||
floor: roomData.floor,
|
||||
room_type_id: roomData.room_type_id,
|
||||
status: roomData.status,
|
||||
featured: roomData.featured,
|
||||
price: roomData.price?.toString() || '',
|
||||
description: roomData.description || '',
|
||||
capacity: roomData.capacity?.toString() || '',
|
||||
room_size: roomData.room_size || '',
|
||||
view: roomData.view || '',
|
||||
amenities: updatedAmenitiesArray,
|
||||
});
|
||||
|
||||
setEditingRoom(roomData);
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch full room details', error);
|
||||
toast.error('Failed to load complete room details');
|
||||
}
|
||||
const handleEditRoom = (room: Room) => {
|
||||
navigate(`/admin/rooms/${room.id}/edit`);
|
||||
};
|
||||
|
||||
const handleDeleteRoom = async (id: number) => {
|
||||
if (!window.confirm('Are you sure you want to delete this room?')) return;
|
||||
const room = contextRooms.find(r => r.id === id) || statusBoardRooms.find(r => r.id === id);
|
||||
const roomNumber = room?.room_number || 'this room';
|
||||
|
||||
if (!window.confirm(`Are you sure you want to delete room ${roomNumber}? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await contextDeleteRoom(id);
|
||||
toast.success(`Room ${roomNumber} deleted successfully`);
|
||||
await refreshStatusBoard();
|
||||
await refreshRooms();
|
||||
// Remove from expanded rooms if it was expanded
|
||||
setExpandedRooms(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
} catch (error: any) {
|
||||
// Error already handled in context
|
||||
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Failed to delete room');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -552,6 +501,8 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
});
|
||||
setSelectedFiles([]);
|
||||
setUploadingImages(false);
|
||||
setCustomAmenityInput('');
|
||||
setEditingAmenity(null);
|
||||
};
|
||||
|
||||
const toggleAmenity = (amenity: string) => {
|
||||
@@ -563,6 +514,86 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddCustomAmenity = () => {
|
||||
const trimmed = customAmenityInput.trim();
|
||||
if (trimmed && !roomFormData.amenities.includes(trimmed)) {
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: [...prev.amenities, trimmed]
|
||||
}));
|
||||
setCustomAmenityInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAmenity = (amenity: string) => {
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: prev.amenities.filter(a => a !== amenity)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleEditAmenity = (amenity: string) => {
|
||||
setEditingAmenity({ name: amenity, newName: amenity });
|
||||
};
|
||||
|
||||
const handleSaveAmenityEdit = async () => {
|
||||
if (!editingAmenity || editingAmenity.name === editingAmenity.newName.trim()) {
|
||||
setEditingAmenity(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const newName = editingAmenity.newName.trim();
|
||||
if (!newName) {
|
||||
toast.error('Amenity name cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await roomService.updateAmenity(editingAmenity.name, newName);
|
||||
toast.success(`Amenity "${editingAmenity.name}" updated to "${newName}"`);
|
||||
|
||||
setAvailableAmenities(prev => {
|
||||
const updated = prev.map(a => a === editingAmenity.name ? newName : a);
|
||||
return updated.sort();
|
||||
});
|
||||
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: prev.amenities.map(a => a === editingAmenity.name ? newName : a)
|
||||
}));
|
||||
|
||||
setEditingAmenity(null);
|
||||
await fetchAvailableAmenities();
|
||||
await refreshRooms();
|
||||
await refreshStatusBoard();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Failed to update amenity');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAmenity = async (amenity: string) => {
|
||||
if (!window.confirm(`Are you sure you want to delete "${amenity}"? This will remove it from all rooms and room types.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await roomService.deleteAmenity(amenity);
|
||||
toast.success(`Amenity "${amenity}" deleted successfully`);
|
||||
|
||||
setAvailableAmenities(prev => prev.filter(a => a !== amenity));
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: prev.amenities.filter(a => a !== amenity)
|
||||
}));
|
||||
|
||||
await fetchAvailableAmenities();
|
||||
await refreshRooms();
|
||||
await refreshStatusBoard();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Failed to delete amenity');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
const files = Array.from(e.target.files);
|
||||
@@ -806,17 +837,29 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
<span>{getStatusLabel(effectiveStatus)}</span>
|
||||
</div>
|
||||
|
||||
{/* Edit Button */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditRoomFromStatusBoard(room.id);
|
||||
}}
|
||||
className="absolute top-3 left-3 p-2 bg-white/90 backdrop-blur-sm rounded-lg text-blue-600 hover:text-blue-700 hover:bg-white transition-all duration-200 shadow-md hover:shadow-lg border border-blue-200 hover:border-blue-300 z-20"
|
||||
title="Edit Room"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
{/* Action Buttons */}
|
||||
<div className="absolute top-3 left-3 flex gap-2 z-20">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditRoomFromStatusBoard(room.id);
|
||||
}}
|
||||
className="p-2 bg-white/90 backdrop-blur-sm rounded-lg text-blue-600 hover:text-blue-700 hover:bg-white transition-all duration-200 shadow-md hover:shadow-lg border border-blue-200 hover:border-blue-300"
|
||||
title="Edit Room"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteRoom(room.id);
|
||||
}}
|
||||
className="p-2 bg-white/90 backdrop-blur-sm rounded-lg text-rose-600 hover:text-rose-700 hover:bg-white transition-all duration-200 shadow-md hover:shadow-lg border border-rose-200 hover:border-rose-300 hover:scale-105"
|
||||
title={`Delete Room ${room.room_number}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Room Content */}
|
||||
<div className="p-5 pt-16 cursor-pointer" onClick={() => toggleRoomExpansion(room.id)}>
|
||||
|
||||
@@ -353,8 +353,9 @@ const AuditLogsPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
{showDetails && selectedLog && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 overflow-y-auto p-4">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-lg max-w-3xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Audit Log Details</h2>
|
||||
@@ -460,6 +461,7 @@ const AuditLogsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -417,8 +417,9 @@ const BannerManagementPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
@@ -440,7 +441,7 @@ const BannerManagementPage: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
|
||||
<form onSubmit={handleSubmit} className="p-4 sm:p-6 space-y-4 sm:space-y-5">
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">
|
||||
@@ -679,6 +680,8 @@ const BannerManagementPage: React.FC = () => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Plus, Search, Edit, Trash2, X, Eye, EyeOff, Loader2, Calendar, User, Tag, GripVertical, Image as ImageIcon, Type, Quote, List, Video, ArrowRight, MoveUp, MoveDown, Sparkles, Upload } from 'lucide-react';
|
||||
import { Plus, Search, Edit, Trash2, X, Eye, EyeOff, Loader2, Tag, GripVertical, Image as ImageIcon, Type, Quote, List, Video, ArrowRight, MoveUp, MoveDown, Sparkles, Upload } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
@@ -42,6 +42,16 @@ const BlogManagementPage: React.FC = () => {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [showSectionBuilder, setShowSectionBuilder] = useState(false);
|
||||
const [uploadingImages, setUploadingImages] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
// Tag management state
|
||||
const [showTagController, setShowTagController] = useState(false);
|
||||
const [tags, setTags] = useState<Array<{ name: string; usage_count: number }>>([]);
|
||||
const [tagsLoading, setTagsLoading] = useState(false);
|
||||
const [editingTag, setEditingTag] = useState<{ old: string; new: string } | null>(null);
|
||||
const [deleteTagConfirm, setDeleteTagConfirm] = useState<{ show: boolean; tag: string | null }>({
|
||||
show: false,
|
||||
tag: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
@@ -51,6 +61,12 @@ const BlogManagementPage: React.FC = () => {
|
||||
fetchPosts();
|
||||
}, [filters, currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showTagController) {
|
||||
fetchTags();
|
||||
}
|
||||
}, [showTagController]);
|
||||
|
||||
const fetchPosts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -275,6 +291,50 @@ const BlogManagementPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Tag management functions
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
setTagsLoading(true);
|
||||
const response = await blogService.getAllTags();
|
||||
if (response.status === 'success' && response.data) {
|
||||
setTags(response.data.tags);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.detail || 'Failed to load tags');
|
||||
} finally {
|
||||
setTagsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameTag = async () => {
|
||||
if (!editingTag || !editingTag.new.trim() || editingTag.old === editingTag.new.trim()) {
|
||||
setEditingTag(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await blogService.renameTag(editingTag.old, editingTag.new.trim());
|
||||
toast.success(response.message || 'Tag renamed successfully');
|
||||
setEditingTag(null);
|
||||
fetchTags();
|
||||
fetchPosts(); // Refresh posts to show updated tags
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.detail || 'Failed to rename tag');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTag = async () => {
|
||||
if (!deleteTagConfirm.tag) return;
|
||||
try {
|
||||
const response = await blogService.deleteTag(deleteTagConfirm.tag);
|
||||
toast.success(response.message || 'Tag deleted successfully');
|
||||
setDeleteTagConfirm({ show: false, tag: null });
|
||||
fetchTags();
|
||||
fetchPosts(); // Refresh posts to show updated tags
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.detail || 'Failed to delete tag');
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return 'Not published';
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
@@ -398,13 +458,22 @@ const BlogManagementPage: React.FC = () => {
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Blog Management</h1>
|
||||
<p className="text-gray-600 mt-1">Manage your blog posts and content</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleOpenModal()}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-white rounded-lg hover:from-[#f5d76e] hover:to-[#d4af37] transition-all shadow-lg"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
<span>New Post</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setShowTagController(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:from-blue-700 hover:to-blue-800 transition-all shadow-lg"
|
||||
>
|
||||
<Tag className="w-5 h-5" />
|
||||
<span>Manage Tags</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleOpenModal()}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-white rounded-lg hover:from-[#f5d76e] hover:to-[#d4af37] transition-all shadow-lg"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
<span>New Post</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -530,8 +599,9 @@ const BlogManagementPage: React.FC = () => {
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-4xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-4xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
@@ -551,7 +621,7 @@ const BlogManagementPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
|
||||
<form onSubmit={handleSubmit} className="p-4 sm:p-6 space-y-5 sm:space-y-6">
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">Title *</label>
|
||||
@@ -1177,6 +1247,7 @@ const BlogManagementPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
@@ -1187,6 +1258,131 @@ const BlogManagementPage: React.FC = () => {
|
||||
title="Delete Blog Post"
|
||||
message="Are you sure you want to delete this blog post? This action cannot be undone."
|
||||
/>
|
||||
|
||||
{/* Tag Controller Modal */}
|
||||
{showTagController && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-4xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="sticky top-0 bg-gradient-to-r from-blue-600 via-blue-700 to-blue-600 px-4 sm:px-6 py-4 sm:py-5 border-b border-blue-800 z-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-lg sm:text-xl md:text-2xl font-bold text-white">
|
||||
Tag Controller
|
||||
</h2>
|
||||
<p className="text-blue-100/80 text-xs sm:text-sm font-light mt-1">
|
||||
Manage all blog tags - rename or delete tags across all posts
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowTagController(false);
|
||||
setEditingTag(null);
|
||||
}}
|
||||
className="w-9 h-9 sm:w-10 sm:h-10 flex items-center justify-center rounded-xl text-white hover:bg-blue-800/50 transition-all duration-200 border border-blue-500 hover:border-blue-300"
|
||||
>
|
||||
<X className="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
|
||||
<div className="p-4 sm:p-6">
|
||||
{tagsLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
||||
</div>
|
||||
) : tags.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Tag className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<p className="text-gray-500 text-lg">No tags found</p>
|
||||
<p className="text-gray-400 text-sm mt-2">Tags will appear here when you add them to blog posts</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{tags.map((tag) => (
|
||||
<div
|
||||
key={tag.name}
|
||||
className="bg-gradient-to-br from-gray-50 to-white border-2 border-gray-200 rounded-xl p-4 hover:border-blue-300 transition-all"
|
||||
>
|
||||
{editingTag && editingTag.old === tag.name ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={editingTag.new}
|
||||
onChange={(e) => setEditingTag({ ...editingTag, new: e.target.value })}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleRenameTag();
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingTag(null);
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
className="flex-1 px-4 py-2 border-2 border-blue-400 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter new tag name"
|
||||
/>
|
||||
<button
|
||||
onClick={handleRenameTag}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingTag(null)}
|
||||
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="w-5 h-5 text-blue-600" />
|
||||
<span className="text-lg font-semibold text-gray-900">{tag.name}</span>
|
||||
</div>
|
||||
<span className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm font-medium">
|
||||
Used in {tag.usage_count} {tag.usage_count === 1 ? 'post' : 'posts'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setEditingTag({ old: tag.name, new: tag.name })}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
<span>Rename</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteTagConfirm({ show: true, tag: tag.name })}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Tag Confirmation */}
|
||||
<ConfirmationDialog
|
||||
isOpen={deleteTagConfirm.show}
|
||||
onClose={() => setDeleteTagConfirm({ show: false, tag: null })}
|
||||
onConfirm={handleDeleteTag}
|
||||
title="Delete Tag"
|
||||
message={`Are you sure you want to delete the tag "${deleteTagConfirm.tag}"? This will remove it from all blog posts. This action cannot be undone.`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -115,7 +115,7 @@ const BookingManagementPage: React.FC = () => {
|
||||
const response = await invoiceService.getInvoicesByBooking(booking.id);
|
||||
|
||||
// Check response structure - handle both possible formats
|
||||
const invoices = response.data?.invoices || response.data?.data?.invoices || [];
|
||||
const invoices = response.data?.invoices || (response.data as any)?.data?.invoices || [];
|
||||
const hasInvoice = Array.isArray(invoices) && invoices.length > 0;
|
||||
|
||||
return {
|
||||
@@ -197,7 +197,7 @@ const BookingManagementPage: React.FC = () => {
|
||||
let invoice = null;
|
||||
if (response.status === 'success' && response.data) {
|
||||
// Try different possible response structures
|
||||
invoice = response.data.invoice || response.data.data?.invoice || response.data;
|
||||
invoice = response.data.invoice || (response.data as any).data?.invoice || response.data;
|
||||
logger.debug('Extracted invoice', { invoice });
|
||||
}
|
||||
|
||||
@@ -637,8 +637,9 @@ const BookingManagementPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
{showDetailModal && selectedBooking && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl w-full max-w-3xl max-h-[95vh] sm:max-h-[90vh] overflow-hidden animate-scale-in border border-slate-200">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl w-full max-w-3xl my-8 max-h-[calc(100vh-4rem)] overflow-hidden animate-scale-in border border-slate-200">
|
||||
{}
|
||||
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-8 py-6 border-b border-slate-700">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -655,7 +656,7 @@ const BookingManagementPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
|
||||
<div className="p-4 sm:p-6 md:p-8 space-y-5 sm:space-y-6">
|
||||
{}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
@@ -985,6 +986,7 @@ const BookingManagementPage: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1044,8 +1044,9 @@ const BusinessDashboardPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
{showPromotionModal && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden border border-gray-200">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-4">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-gray-200">
|
||||
{}
|
||||
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-8 py-6 border-b border-slate-700">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -1067,7 +1068,7 @@ const BusinessDashboardPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="p-8 overflow-y-auto max-h-[calc(90vh-120px)] custom-scrollbar">
|
||||
<div className="p-8 overflow-y-auto max-h-[calc(100vh-12rem)] custom-scrollbar">
|
||||
<form onSubmit={handlePromotionSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
@@ -1241,7 +1242,8 @@ const BusinessDashboardPage: React.FC = () => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
1044
Frontend/src/pages/admin/EditRoomPage.tsx
Normal file
1044
Frontend/src/pages/admin/EditRoomPage.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -753,8 +753,9 @@ const LoyaltyManagementPage: React.FC = () => {
|
||||
|
||||
{/* Tier Modal */}
|
||||
{showTierModal && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
@@ -776,7 +777,7 @@ const LoyaltyManagementPage: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
|
||||
<form onSubmit={handleTierSubmit} className="p-4 sm:p-6 space-y-4 sm:space-y-5">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
@@ -919,13 +920,15 @@ const LoyaltyManagementPage: React.FC = () => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reward Modal */}
|
||||
{showRewardModal && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-3xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-3xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
@@ -947,7 +950,7 @@ const LoyaltyManagementPage: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
|
||||
<form onSubmit={handleRewardSubmit} className="p-4 sm:p-6 space-y-4 sm:space-y-5">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
@@ -1149,6 +1152,7 @@ const LoyaltyManagementPage: React.FC = () => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -315,8 +315,9 @@ const PromotionManagementPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-4 animate-fade-in">
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden border border-slate-200 animate-scale-in">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200 animate-scale-in">
|
||||
{}
|
||||
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-8 py-6 border-b border-slate-700">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -338,7 +339,7 @@ const PromotionManagementPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="p-8 overflow-y-auto max-h-[calc(90vh-120px)] custom-scrollbar">
|
||||
<div className="p-8 overflow-y-auto max-h-[calc(100vh-12rem)] custom-scrollbar">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
@@ -512,6 +513,8 @@ const PromotionManagementPage: React.FC = () => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -475,8 +475,9 @@ const IPWhitelistTab: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-md w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-md w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
@@ -491,7 +492,7 @@ const IPWhitelistTab: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
|
||||
<div className="p-4 sm:p-6 space-y-4 sm:space-y-5">
|
||||
<input
|
||||
type="text"
|
||||
@@ -649,8 +650,9 @@ const IPBlacklistTab: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-md w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-md w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
@@ -665,7 +667,7 @@ const IPBlacklistTab: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
|
||||
<div className="p-4 sm:p-6 space-y-4 sm:space-y-5">
|
||||
<input
|
||||
type="text"
|
||||
@@ -888,8 +890,9 @@ const OAuthProvidersTab: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
|
||||
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
@@ -908,7 +911,7 @@ const OAuthProvidersTab: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
|
||||
<div className="p-4 sm:p-6 space-y-3 sm:space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
@@ -1352,8 +1355,9 @@ const GDPRRequestsTab: React.FC = () => {
|
||||
|
||||
{/* Request Details Modal */}
|
||||
{selectedRequest && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl p-4 sm:p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="fixed inset-0 bg-black/50 z-50 overflow-y-auto p-4">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-xl p-4 sm:p-6 max-w-2xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-y-auto">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h4 className="text-base sm:text-lg font-semibold">GDPR Request Details</h4>
|
||||
<button
|
||||
@@ -1399,6 +1403,7 @@ const GDPRRequestsTab: React.FC = () => {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -240,8 +240,9 @@ const ServiceManagementPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-4 animate-fade-in">
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md max-h-[90vh] overflow-hidden border border-slate-200 animate-scale-in">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200 animate-scale-in">
|
||||
{}
|
||||
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-6 py-5 border-b border-slate-700">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -344,6 +345,7 @@ const ServiceManagementPage: React.FC = () => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -388,8 +388,9 @@ const UserManagementPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl w-full max-w-md max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200 animate-scale-in">
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
|
||||
<div className="min-h-full flex items-start justify-center py-8">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl w-full max-w-md my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200 animate-scale-in">
|
||||
{}
|
||||
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-6 py-5 border-b border-slate-700">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -410,7 +411,7 @@ const UserManagementPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
|
||||
<form onSubmit={handleSubmit} className="p-4 sm:p-6 space-y-4 sm:space-y-5">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 uppercase tracking-wider mb-2">
|
||||
@@ -512,6 +513,7 @@ const UserManagementPage: React.FC = () => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user