feat: tạo hook useNotification.tsx để dùng chung toàn hệ thống

This commit is contained in:
2026-06-16 12:41:00 +07:00
parent 29d39ae7b0
commit bfd18e05dd
9 changed files with 241 additions and 422 deletions
+12 -12
View File
@@ -7,7 +7,7 @@ import 'leaflet/dist/leaflet.css';
import { useTourStore } from '@/store/useTourStore';
import { X, Navigation, Image as ImageIcon, LogOut, Settings, Edit2, Trash2, Share2, Filter, Tag as TagIcon } from 'lucide-react';
import { UserManagementModal } from '@/components/UserManagementModal';
import { NotificationModal, useNotificationModal } from '@/components/NotificationModal';
import { useNotification } from '@/hooks/useNotification';
import { CreateTourModal } from '../components/CreateTourModal';
// Fix lỗi icon mặc định của Leaflet
@@ -63,7 +63,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
const fetchTour = useTourStore(state => state.fetchTour);
const setMapCenter = useTourStore(state => state.setMapCenter);
const notificationModal = useNotificationModal();
const notify = useNotification();
// Khởi tạo vị trí từ localStorage nếu có, nếu không dùng mặc định (TP.HCM)
const [initialViewState] = useState(() => {
@@ -104,7 +104,11 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
}).catch(() => {});
} else if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(shareUrl).then(() => {
notificationModal.openModal('Thành công', 'Đã sao chép liên kết chuyến đi vào bộ nhớ tạm. Bạn có thể gửi cho bạn bè ngay!', 'success');
notify({
title: 'Thành công',
message: 'Đã sao chép liên kết chuyến đi vào bộ nhớ tạm. Bạn có thể gửi cho bạn bè ngay!',
type: 'success'
});
});
} else {
// Giải pháp dự phòng cho môi trường không có HTTPS
@@ -114,7 +118,11 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
textArea.select();
try {
document.execCommand('copy');
notificationModal.openModal('Thành công', 'Đã sao chép liên kết chuyến đi vào bộ nhớ tạm. Bạn có thể gửi cho bạn bè ngay!', 'success');
notify({
title: 'Thành công',
message: 'Đã sao chép liên kết chuyến đi vào bộ nhớ tạm. Bạn có thể gửi cho bạn bè ngay!',
type: 'success'
});
} catch (err) {}
document.body.removeChild(textArea);
}
@@ -346,14 +354,6 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
onViewTour(tour.id);
}}
/>
<NotificationModal
isOpen={notificationModal.modalState?.isOpen ?? false}
title={notificationModal.modalState?.title}
message={notificationModal.modalState?.message}
type={notificationModal.modalState?.type}
onConfirm={() => notificationModal.closeModal()}
/>
</div>
);
};
+73 -83
View File
@@ -5,8 +5,8 @@ import { ExpenseManager } from '../components/ExpenseManager';
import { useTourStore } from '@/store/useTourStore';
import { AddLocationModal } from '@/components/AddLocationModal';
import { AddMemberModal } from '../components/AddMemberModal';
import { ConfirmModal } from '../components/ConfirmModal';
import { NotificationModal, useNotificationModal } from '@/components/NotificationModal';
import { useConfirm } from '@/hooks/useConfirm';
import { useNotification } from '@/hooks/useNotification';
import { CommentModal } from '@/components/CommentModal';
import { MapContainer, TileLayer, Marker, Popup, useMapEvents, Polyline, useMap } from 'react-leaflet';
import _MarkerClusterGroup from 'react-leaflet-cluster';
@@ -197,7 +197,6 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
const [commentLocationId, setCommentLocationId] = useState('');
const [commentLocationName, setCommentLocationName] = useState('');
const [joinRequestActionId, setJoinRequestActionId] = useState<string | null>(null);
const [confirmState, setConfirmState] = useState<{ open: boolean; title?: string; message?: string; onConfirm?: () => void }>({ open: false });
const fetchTour = useTourStore(state => state.fetchTour);
const fetchPublicTourDetails = useTourStore(state => state.fetchPublicTourDetails); // New action
@@ -248,7 +247,8 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
const acceptJoinRequest = useTourStore(state => state.acceptJoinRequest);
const rejectJoinRequest = useTourStore(state => state.rejectJoinRequest);
const notificationModal = useNotificationModal();
const confirm = useConfirm();
const notify = useNotification();
// Khôi phục vị trí và mức zoom từ localStorage
const [initialViewState] = useState(() => {
@@ -304,7 +304,11 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
}).catch(() => {});
} else if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(shareUrl).then(() => {
notificationModal.openModal('Thành công', 'Đã sao chép liên kết chia sẻ chuyến đi!', 'success');
notify({
title: 'Thành công',
message: 'Đã sao chép liên kết chia sẻ chuyến đi!',
type: 'success'
});
});
} else {
// Giải pháp dự phòng cho môi trường không có HTTPS (truy cập qua IP)
@@ -314,7 +318,11 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
textArea.select();
try {
document.execCommand('copy');
notificationModal.openModal('Thành công', 'Đã sao chép liên kết chia sẻ chuyến đi!', 'success');
notify({
title: 'Thành công',
message: 'Đã sao chép liên kết chia sẻ chuyến đi!',
type: 'success'
});
} catch (err) {}
document.body.removeChild(textArea);
}
@@ -450,10 +458,14 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
childDiscount: childDiscountInput,
tags: tagsInput
});
notificationModal.openModal('Thành công', 'Đã cập nhật thông tin chuyến đi.', 'success');
notify({
title: 'Thành công',
message: 'Đã cập nhật thông tin chuyến đi.',
type: 'success'
});
fetchTour(currentTour.id); // Re-fetch tour để cập nhật lại các tính toán liên quan
} catch (error: any) {
notificationModal.openModal('Lỗi', error.message || 'Không thể cập nhật thông tin.', 'error');
notify({ title: 'Lỗi', message: error.message || 'Không thể cập nhật thông tin.', type: 'error' });
}
};
// Xác định Điểm xuất phát và Điểm kết thúc hiển thị dưới widget tài chính
@@ -609,23 +621,21 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
onClick={async (e) => {
e.stopPropagation();
if (!currentTour) return;
setConfirmState({
open: true,
const isConfirmed = await confirm({
title: 'Chấp nhận yêu cầu',
message: `Chấp nhận ${req.user?.name || req.userId} vào tour?`,
onConfirm: async () => {
setJoinRequestActionId(req.id);
try {
await acceptJoinRequest(currentTour.id, req.id);
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
} catch (e: any) {
notificationModal.openModal('Thông báo', e.message || 'Không thể chấp nhận yêu cầu', 'error');
} finally {
setJoinRequestActionId(null);
setConfirmState({ open: false });
}
},
message: `Chấp nhận ${req.user?.name || req.userId} vào tour?`
});
if (isConfirmed) {
setJoinRequestActionId(req.id);
try {
await acceptJoinRequest(currentTour.id, req.id);
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
} catch (e: any) {
notify({ title: 'Thông báo', message: e.message || 'Không thể chấp nhận yêu cầu', type: 'error' });
} finally {
setJoinRequestActionId(null);
}
}
}}
className="w-4 h-4 bg-green-500 hover:bg-green-600 rounded-full flex items-center justify-center text-white text-[10px] leading-none disabled:opacity-50"
aria-label="Accept"
@@ -638,23 +648,21 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
onClick={async (e) => {
e.stopPropagation();
if (!currentTour) return;
setConfirmState({
open: true,
const isConfirmed = await confirm({
title: 'Từ chối yêu cầu',
message: `Từ chối ${req.user?.name || req.userId} tham gia tour?`,
onConfirm: async () => {
setJoinRequestActionId(req.id);
try {
await rejectJoinRequest(currentTour.id, req.id);
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
} catch (e: any) {
notificationModal.openModal('Thông báo', e.message || 'Không thể từ chối yêu cầu', 'error');
} finally {
setJoinRequestActionId(null);
setConfirmState({ open: false });
}
},
message: `Từ chối ${req.user?.name || req.userId} tham gia tour?`
});
if (isConfirmed) {
setJoinRequestActionId(req.id);
try {
await rejectJoinRequest(currentTour.id, req.id);
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
} catch (e: any) {
notify({ title: 'Thông báo', message: e.message || 'Không thể từ chối yêu cầu', type: 'error' });
} finally {
setJoinRequestActionId(null);
}
}
}}
className="w-4 h-4 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center text-white text-[10px] leading-none disabled:opacity-50 -ml-1"
aria-label="Reject"
@@ -905,23 +913,21 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
disabled={joinRequestActionId === req.id}
onClick={async () => {
if (!currentTour) return;
setConfirmState({
open: true,
const isConfirmed = await confirm({
title: 'Chấp nhận yêu cầu',
message: `Chấp nhận ${req.user?.name || req.userId} vào tour?`,
onConfirm: async () => {
setJoinRequestActionId(req.id);
try {
await acceptJoinRequest(currentTour.id, req.id);
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
} catch (e: any) {
notificationModal.openModal('Thông báo', e.message || 'Không thể chấp nhận yêu cầu', 'error');
} finally {
setJoinRequestActionId(null);
setConfirmState({ open: false });
}
},
message: `Chấp nhận ${req.user?.name || req.userId} vào tour?`
});
if (isConfirmed) {
setJoinRequestActionId(req.id);
try {
await acceptJoinRequest(currentTour.id, req.id);
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
} catch (e: any) {
notify({ title: 'Thông báo', message: e.message || 'Không thể chấp nhận yêu cầu', type: 'error' });
} finally {
setJoinRequestActionId(null);
}
}
}}
className="p-2 rounded-xl bg-green-50 text-green-700 hover:bg-green-100 disabled:opacity-50"
aria-label="Accept"
@@ -932,23 +938,21 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
disabled={joinRequestActionId === req.id}
onClick={async () => {
if (!currentTour) return;
setConfirmState({
open: true,
const isConfirmed = await confirm({
title: 'Từ chối yêu cầu',
message: `Từ chối ${req.user?.name || req.userId} tham gia tour?`,
onConfirm: async () => {
setJoinRequestActionId(req.id);
try {
await rejectJoinRequest(currentTour.id, req.id);
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
} catch (e: any) {
notificationModal.openModal('Thông báo', e.message || 'Không thể từ chối yêu cầu', 'error');
} finally {
setJoinRequestActionId(null);
setConfirmState({ open: false });
}
},
message: `Từ chối ${req.user?.name || req.userId} tham gia tour?`
});
if (isConfirmed) {
setJoinRequestActionId(req.id);
try {
await rejectJoinRequest(currentTour.id, req.id);
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
} catch (e: any) {
notify({ title: 'Thông báo', message: e.message || 'Không thể từ chối yêu cầu', type: 'error' });
} finally {
setJoinRequestActionId(null);
}
}
}}
className="p-2 rounded-xl bg-red-50 text-red-700 hover:bg-red-100 disabled:opacity-50"
aria-label="Reject"
@@ -1168,7 +1172,7 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
await removeMember(currentTour.id, selectedMember.userId);
setIsMemberDetailOpen(false);
} catch (e) {
notificationModal.openModal('Thông báo', 'Không thể xóa thành viên', 'error');
notify({ title: 'Thông báo', message: 'Không thể xóa thành viên', type: 'error' });
}
}}
className="px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded-xl text-sm font-bold"
@@ -1191,20 +1195,6 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
</div>
</div>
)}
<ConfirmModal
isOpen={confirmState.open}
title={confirmState.title}
message={confirmState.message}
onConfirm={() => confirmState.onConfirm?.()}
onCancel={() => setConfirmState({ open: false })}
/>
<NotificationModal
isOpen={notificationModal.modalState?.isOpen ?? false}
title={notificationModal.modalState?.title}
message={notificationModal.modalState?.message}
type={notificationModal.modalState?.type}
onConfirm={() => notificationModal.closeModal()}
/>
<CommentModal
isOpen={isCommentModalOpen}
onClose={() => setIsCommentModalOpen(false)}