fix: guest and admin logic

This commit is contained in:
2026-06-22 11:41:34 +07:00
parent b1a539235b
commit 860395cb14
61 changed files with 1291 additions and 244 deletions
+83 -57
View File
@@ -1,46 +1,61 @@
const loadScript = (src: string, fallbackSrc?: string): Promise<void> => {
const loadScript = (src: string, fallbackSrcs?: string[]): Promise<void> => {
return new Promise((resolve, reject) => {
if (
document.querySelector(`script[src="${src}"]`) ||
(fallbackSrc && document.querySelector(`script[src="${fallbackSrc}"]`))
) {
const allSrcs = [src, ...(fallbackSrcs || [])];
// Check if any of the scripts are already loaded
if (allSrcs.some(s => document.querySelector(`script[src="${s}"]`))) {
resolve();
return;
}
const script = document.createElement('script');
script.src = src;
script.onload = () => resolve();
script.onerror = () => {
if (fallbackSrc) {
console.warn(`Failed to load script ${src}. Trying fallback: ${fallbackSrc}`);
const fallbackScript = document.createElement('script');
fallbackScript.src = fallbackSrc;
fallbackScript.onload = () => resolve();
fallbackScript.onerror = () => reject(new Error(`Failed to load script ${fallbackSrc}`));
document.head.appendChild(fallbackScript);
} else {
reject(new Error(`Failed to load script ${src}`));
const tryLoadScript = (index: number) => {
if (index >= allSrcs.length) {
reject(new Error(`Failed to load script from any source: ${allSrcs.join(', ')}`));
return;
}
const currentSrc = allSrcs[index];
const script = document.createElement('script');
script.src = currentSrc;
script.onload = () => resolve();
script.onerror = () => {
console.warn(`Failed to load script ${currentSrc}. Trying next fallback...`);
const nextIndex = index + 1;
if (nextIndex < allSrcs.length) {
tryLoadScript(nextIndex);
} else {
reject(new Error(`Failed to load script from all sources: ${allSrcs.join(', ')}`));
}
};
document.head.appendChild(script);
};
document.head.appendChild(script);
tryLoadScript(0);
});
};
const loadModerationLibraries = async () => {
// Load TensorFlow first with fallback
// Load TensorFlow first with fallbacks
await loadScript(
'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs',
'https://unpkg.com/@tensorflow/tfjs'
['https://unpkg.com/@tensorflow/tfjs', 'https://esm.sh/@tensorflow/tfjs']
);
// Load models after tfjs is available, with fallbacks
// Load models after tfjs is available, with multiple fallbacks
await Promise.all([
loadScript(
'https://cdn.jsdelivr.net/npm/@tensorflow-models/blazeface',
'https://unpkg.com/@tensorflow-models/blazeface'
['https://unpkg.com/@tensorflow-models/blazeface', 'https://esm.sh/@tensorflow-models/blazeface']
),
// NSFWJS with 3 CDN fallbacks
loadScript(
'https://cdn.jsdelivr.net/npm/nsfwjs@2.4.0/dist/bundle.js',
'https://unpkg.com/nsfwjs@2.4.0/dist/bundle.js'
[
'https://unpkg.com/nsfwjs@2.4.0/dist/bundle.js',
'https://esm.sh/nsfwjs@2.4.0/dist/bundle.js'
]
)
]);
};
@@ -56,7 +71,13 @@ export const processImageModeration = async (file: File): Promise<{ file: File;
return { file, blocked: false };
}
await loadModerationLibraries();
// Try to load moderation libraries, but don't fail if they're unavailable
try {
await loadModerationLibraries();
} catch (libLoadErr) {
console.warn('Moderation libraries failed to load, proceeding without NSFW/Face blur checks:', libLoadErr);
return { file, blocked: false };
}
return new Promise((resolve) => {
const img = new Image();
@@ -73,48 +94,53 @@ export const processImageModeration = async (file: File): Promise<{ file: File;
if (blockNsfw) {
try {
const nsfwModel = await (window as any).nsfwjs.load();
const predictions = await nsfwModel.classify(canvas);
const pornOrHentai = predictions.find((p: any) => p.className === 'Porn' || p.className === 'Hentai');
const pornProb = pornOrHentai ? pornOrHentai.probability : 0;
if (pornProb > 0.5) {
resolve({ file, blocked: true });
return;
const nsfwModel = await (window as any).nsfwjs?.load();
if (nsfwModel) {
const predictions = await nsfwModel.classify(canvas);
const pornOrHentai = predictions.find((p: any) => p.className === 'Porn' || p.className === 'Hentai');
const pornProb = pornOrHentai ? pornOrHentai.probability : 0;
if (pornProb > 0.5) {
console.warn(`Image blocked by NSFW filter (probability: ${pornProb})`);
resolve({ file, blocked: true });
return;
}
}
} catch (e) {
console.error('NSFW validation error:', e);
console.warn('NSFW validation error (will allow upload):', e);
}
}
let modified = false;
if (blurFaces) {
try {
const blazefaceModel = await (window as any).blazeface.load();
const predictions = await blazefaceModel.estimateFaces(canvas, false);
if (predictions && predictions.length > 0) {
modified = true;
predictions.forEach((prediction: any) => {
const startX = prediction.topLeft[0];
const startY = prediction.topLeft[1];
const endX = prediction.bottomRight[0];
const endY = prediction.bottomRight[1];
const width = endX - startX;
const height = endY - startY;
const blazefaceModel = await (window as any).blazeface?.load();
if (blazefaceModel) {
const predictions = await blazefaceModel.estimateFaces(canvas, false);
if (predictions && predictions.length > 0) {
modified = true;
predictions.forEach((prediction: any) => {
const startX = prediction.topLeft[0];
const startY = prediction.topLeft[1];
const endX = prediction.bottomRight[0];
const endY = prediction.bottomRight[1];
const width = endX - startX;
const height = endY - startY;
const faceCanvas = document.createElement('canvas');
faceCanvas.width = width;
faceCanvas.height = height;
const faceCtx = faceCanvas.getContext('2d');
if (faceCtx) {
faceCtx.drawImage(canvas, startX, startY, width, height, 0, 0, width, height);
ctx.filter = 'blur(15px)';
ctx.drawImage(faceCanvas, startX, startY, width, height);
ctx.filter = 'none';
}
});
const faceCanvas = document.createElement('canvas');
faceCanvas.width = width;
faceCanvas.height = height;
const faceCtx = faceCanvas.getContext('2d');
if (faceCtx) {
faceCtx.drawImage(canvas, startX, startY, width, height, 0, 0, width, height);
ctx.filter = 'blur(15px)';
ctx.drawImage(faceCanvas, startX, startY, width, height);
ctx.filter = 'none';
}
});
}
}
} catch (e) {
console.error('Face blur error:', e);
console.warn('Face blur error (will skip face detection):', e);
}
}
@@ -137,7 +163,7 @@ export const processImageModeration = async (file: File): Promise<{ file: File;
img.src = URL.createObjectURL(file);
});
} catch (err) {
console.error('Image moderation failed:', err);
console.error('Image moderation process failed:', err);
return { file, blocked: false };
}
};
+157 -1
View File
@@ -18,6 +18,10 @@ const translations: Record<string, Record<Language, string>> = {
info: { vi: 'Thông tin', en: 'Info', zh: '信息' },
delete: { vi: 'Xóa', en: 'Delete', zh: '删除' },
edit: { vi: 'Chỉnh sửa', en: 'Edit', zh: '编辑' },
yes: { vi: 'Có', en: 'Yes', zh: '是' },
no: { vi: 'Không', en: 'No', zh: '否' },
close: { vi: 'Đóng', en: 'Close', zh: '关闭' },
ok: { vi: 'Đồng ý', en: 'OK', zh: '确定' },
// Landing Page
welcomeBack: { vi: 'Chào mừng bạn quay trở lại!', en: 'Welcome back!', zh: '欢迎回来!' },
@@ -31,6 +35,7 @@ const translations: Record<string, Record<Language, string>> = {
momentsTitle: { vi: 'Khoảnh khắc cộng đồng', en: 'Community Moments', zh: '社区精彩瞬间' },
trustedMembers: { vi: 'Thành viên uy tín', en: 'Trusted Members', zh: '信用会员' },
exploreToursBtn: { vi: 'Khám phá các hành trình du lịch', en: 'Explore Travel Itineraries', zh: '探索旅行行程' },
exploreTourMap: { vi: 'Khám phá Bản đồ Tour', en: 'Explore Tour Map', zh: '探索旅游地图' },
// Explore Map
systemBtn: { vi: 'Hệ thống', en: 'System', zh: '系统管理' },
@@ -38,6 +43,9 @@ const translations: Record<string, Record<Language, string>> = {
chooseLocationMap: { vi: 'Chọn vị trí trên bản đồ', en: 'Choose location on map', zh: '在地图上选择位置' },
clickMapSelectCoords: { vi: 'Click lên bản đồ để chọn tọa độ', en: 'Click on map to select coordinates', zh: '在地图上点击以选择坐标' },
coordsLabel: { vi: 'Tọa độ', en: 'Coordinates', zh: '坐标' },
businessRestaurant: { vi: 'Nhà hàng', en: 'Restaurant', zh: '餐厅' },
businessHotel: { vi: 'Khách sạn', en: 'Hotel', zh: '酒店' },
businessHomestay: { vi: 'Homestay', en: 'Homestay', zh: 'Homestay' },
noCoordsSelected: { vi: 'Chưa chọn vị trí', en: 'No location selected', zh: '未选择位置' },
searchPlaceholder: { vi: 'Tìm kiếm địa điểm...', en: 'Search places...', zh: '搜索地点...' },
@@ -134,7 +142,155 @@ const translations: Record<string, Record<Language, string>> = {
submitReport: { vi: 'Gửi báo cáo', en: 'Submit Report', zh: '提交举报' },
reportSuccess: { vi: 'Gửi báo cáo thành công. Ban quản trị sẽ kiểm duyệt thông tin.', en: 'Report submitted successfully. The admin will review it.', zh: '提交成功。管理员将审核' },
emptyBlacklist: { vi: 'Chưa có cơ sở nào trong danh sách đen.', en: 'No businesses in blacklist yet.', zh: '黑名单中暂无商家。' },
tabReports: { vi: 'Blacklist', en: 'Blacklist', zh: '黑名单' }
tabReports: { vi: 'Blacklist', en: 'Blacklist', zh: '黑名单' },
// Modal titles and messages
areYouSure: { vi: 'Bạn có chắc chắn không?', en: 'Are you sure?', zh: '你确定吗?' },
processing: { vi: 'Đang xử lý...', en: 'Processing...', zh: '处理中...' },
checkingImages: { vi: 'Đang kiểm tra và lọc hình ảnh của bạn...', en: 'Checking and filtering your images...', zh: '正在检查和过滤您的图片...' },
uploadFailed: { vi: 'Tải ảnh thất bại.', en: 'Image upload failed.', zh: '图片上传失败。' },
uploadSuccess: { vi: 'Đã tải lên thành công.', en: 'Upload successful.', zh: '上传成功。' },
imageBlocked: { vi: 'Ảnh chứa nội dung không phù hợp và bị chặn.', en: 'Image contains inappropriate content and was blocked.', zh: '图片包含不当内容并已被屏蔽。' },
selectImage: { vi: 'Nhấn để chọn ảnh', en: 'Click to select images', zh: '点击选择图片' },
uploadPhotos: { vi: 'Tải ảnh lên', en: 'Upload Photos', zh: '上传照片' },
selectedFiles: { vi: 'Đã chọn', en: 'Selected', zh: '已选择' },
// Photo modals
imageModeration: { vi: 'Ảnh bị từ chối', en: 'Image Rejected', zh: '图片被拒' },
imageModerationBlocked: { vi: 'ảnh chứa nội dung không phù hợp và bị chặn.', en: 'image contains inappropriate content and was blocked.', zh: '图片包含不当内容并已被屏蔽。' },
imageCheck: { vi: 'Lỗi ảnh', en: 'Image Error', zh: '图片错误' },
imageLoadFailed: { vi: 'Không thể đọc nội dung ảnh: ', en: 'Cannot read image content: ', zh: '无法读取图片内容:' },
pleaseTryAgain: { vi: 'Vui lòng kiểm tra lại file.', en: 'Please check the file.', zh: '请检查文件。' },
fileCheckingError: { vi: 'Lỗi trong quá trình kiểm duyệt ảnh.', en: 'Error during image review.', zh: '图片审核过程中出错。' },
// Create Tour Modal
createTourModalTitle: { vi: 'Tạo Tour mới', en: 'Create New Tour', zh: '创建新行程' },
creatingTour: { vi: 'Đang tạo...', en: 'Creating...', zh: '创建中...' },
confirmCreateTour: { vi: 'Xác nhận tạo Tour', en: 'Confirm Tour Creation', zh: '确认创建行程' },
enterTourName: { vi: 'Nhập tên tour...', en: 'Enter tour name...', zh: '输入行程名称...' },
tourNameRequired: { vi: 'Tên tour không được để trống', en: 'Tour name cannot be empty', zh: '行程名称不能为空' },
// Add Location Modal
confirmStartPoint: { vi: 'Xác nhận Điểm xuất phát', en: 'Confirm Starting Point', zh: '确认起点' },
confirmEndPoint: { vi: 'Xác nhận Điểm kết thúc', en: 'Confirm Ending Point', zh: '确认终点' },
location: { vi: 'Vị trí', en: 'Location', zh: '位置' },
// Expense Manager
expenseReportTitle: { vi: 'BÁO CÁO CHI PHÍ TOUR', en: 'TOUR EXPENSE REPORT', zh: '行程费用报告' },
expenseSplitTable: { vi: 'Bảng phân chia chi phí', en: 'Expense Split Table', zh: '费用分割表' },
member: { vi: 'Thành viên', en: 'Member', zh: '成员' },
shouldPay: { vi: 'Cần trả', en: 'Should Pay', zh: '应付' },
paid: { vi: 'Đã trả', en: 'Paid', zh: '已付' },
balance: { vi: 'Số dư', en: 'Balance', zh: '余额' },
// Members Management
mergeMembers: { vi: 'Hợp nhất thành viên', en: 'Merge Members', zh: '合并成员' },
mergeSuccess: { vi: 'Hợp nhất thành công.', en: 'Merge successful.', zh: '合并成功。' },
mergeFailed: { vi: 'Hợp nhất thất bại.', en: 'Merge failed.', zh: '合并失败。' },
// Dashboard / User interactions
disconnect: { vi: 'Hủy kết nối', en: 'Disconnect', zh: '断开连接' },
notificationsMuted: { vi: 'Đã tắt thông báo', en: 'Notifications muted', zh: '通知已关闭' },
searchContent: { vi: 'Tìm kiếm nội dung...', en: 'Search content...', zh: '搜索内容...' },
// Notes Page
noNotesYet: { vi: 'Chưa có ghi chú nào', en: 'No notes yet', zh: '还没有笔记' },
createNewNote: { vi: 'Tạo ghi chú mới', en: 'Create New Note', zh: '创建新笔记' },
noteTitle: { vi: 'Tiêu đề', en: 'Title', zh: '标题' },
noteContent: { vi: 'Nội dung', en: 'Content', zh: '内容' },
deleteNote: { vi: 'Xóa ghi chú', en: 'Delete Note', zh: '删除笔记' },
deleteNoteConfirm: { vi: 'Bạn có chắc chắn muốn xóa ghi chú này?', en: 'Are you sure you want to delete this note?', zh: '确定要删除此笔记吗?' },
// Signup Page
createAccount: { vi: 'Tạo tài khoản mới', en: 'Create New Account', zh: '创建新账户' },
verifyAccount: { vi: 'Xác thực tài khoản', en: 'Verify Account', zh: '验证账户' },
confirmPassword: { vi: 'Xác nhận mật khẩu', en: 'Confirm Password', zh: '确认密码' },
passwordMismatch: { vi: 'Mật khẩu không khớp', en: 'Passwords do not match', zh: '密码不匹配' },
firstName: { vi: 'Tên', en: 'First Name', zh: '名字' },
lastName: { vi: 'Họ', en: 'Last Name', zh: '姓氏' },
// Tour Detail Page
tourMembers: { vi: 'Thành viên', en: 'Members', zh: '成员' },
expenses: { vi: 'Chi phí', en: 'Expenses', zh: '费用' },
errorLoadingTour: { vi: 'Lỗi khi tải thông tin tour.', en: 'Error loading tour information.', zh: '加载行程信息出错。' },
// Explore Map
restaurant: { vi: 'Nhà hàng', en: 'Restaurant', zh: '餐厅' },
hotel: { vi: 'Khách sạn', en: 'Hotel', zh: '酒店' },
homestay: { vi: 'Homestay', en: 'Homestay', zh: '民宿' },
// User Management Modal
businessTypeLabel: { vi: 'Loại hình kinh doanh', en: 'Business Type', zh: '商家类型' },
// Add Member Modal
searchMember: { vi: 'Tìm kiếm thành viên...', en: 'Search members...', zh: '搜索成员...' },
addMemberTitle: { vi: 'Thêm thành viên', en: 'Add Member', zh: '添加成员' },
// Comment Modal
comments: { vi: 'Bình luận', en: 'Comments', zh: '评论' },
addComment: { vi: 'Thêm bình luận', en: 'Add comment', zh: '添加评论' },
writeComment: { vi: 'Viết bình luận...', en: 'Write a comment...', zh: '写评论...' },
noComments: { vi: 'Chưa có bình luận nào', en: 'No comments yet', zh: '还没有评论' },
deleteComment: { vi: 'Xóa bình luận', en: 'Delete comment', zh: '删除评论' },
editComment: { vi: 'Chỉnh sửa bình luận', en: 'Edit comment', zh: '编辑评论' },
// General messages
loading_msg: { vi: 'Đang tải...', en: 'Loading...', zh: '加载中...' },
noData: { vi: 'Không có dữ liệu', en: 'No data', zh: '无数据' },
retry: { vi: 'Thử lại', en: 'Retry', zh: '重试' },
back: { vi: 'Quay lại', en: 'Back', zh: '返回' },
next: { vi: 'Tiếp theo', en: 'Next', zh: '下一步' },
previous: { vi: 'Trước đó', en: 'Previous', zh: '上一步' },
done: { vi: 'Xong', en: 'Done', zh: '完成' },
finish: { vi: 'Kết thúc', en: 'Finish', zh: '完成' },
submit: { vi: 'Gửi', en: 'Submit', zh: '提交' },
update: { vi: 'Cập nhật', en: 'Update', zh: '更新' },
create: { vi: 'Tạo', en: 'Create', zh: '创建' },
new: { vi: 'Mới', en: 'New', zh: '新建' },
// Dashboard - Connections & Friends
friendsList: { vi: 'Danh sách bạn bè', en: 'Friends List', zh: '好友列表' },
familyGroup: { vi: 'Gia đình', en: 'Family', zh: '家庭' },
friends: { vi: 'Bạn bè', en: 'Friends', zh: '朋友' },
manageFriendsDesc: { vi: 'Bạn bè, gia đình, yêu cầu chờ duyệt', en: 'Friends, family, pending requests', zh: '朋友、家人、待决批准' },
noConnections: { vi: 'Bạn chưa kết nối với ai. Hãy chuyển sang tìm kiếm để gửi lời mời.', en: 'You have no connections yet. Search to send invitations.', zh: '你还没有任何连接。搜索以发送邀请。' },
searchMembers: { vi: 'Tìm kiếm theo Tên hoặc Email (nhập tối thiểu 2 ký tự)...', en: 'Search by Name or Email (min 2 characters)...', zh: '按名称或电子邮件搜索(最少2个字符)...' },
searching: { vi: 'Đang tìm kiếm...', en: 'Searching...', zh: '搜索中...' },
minCharsRequired: { vi: 'Vui lòng nhập tối thiểu 2 ký tự để tìm kiếm thành viên.', en: 'Please enter at least 2 characters to search.', zh: '请输入至少2个字符进行搜索。' },
disconnectTitle: { vi: 'Hủy kết nối', en: 'Disconnect', zh: '断开连接' },
disconnectConfirm: { vi: 'Bạn có chắc chắn muốn hủy kết nối với', en: 'Are you sure you want to disconnect with', zh: '你确定要与...断开连接吗' },
disconnectSuccess: { vi: 'Đã hủy kết nối thành công.', en: 'Disconnected successfully.', zh: '断开连接成功。' },
searchError: { vi: 'Lỗi tìm kiếm thành viên:', en: 'Error searching members:', zh: '搜索成员时出错:' },
disconnectError: { vi: 'Lỗi hủy kết nối:', en: 'Error disconnecting:', zh: '断开连接出错:' },
// Dashboard - Photo Gallery
photoGallery: { vi: 'Thư viện ảnh', en: 'Photo Gallery', zh: '相册' },
photoGalleryDesc: { vi: 'Kho ảnh gốc của bạn từ các tour', en: 'Your original photos from all tours', zh: '您来自所有行程的原始照片' },
flagPhoto: { vi: 'Báo cáo ảnh', en: 'Report Photo', zh: '举报照片' },
flagPhotoReason: { vi: 'Lý do báo cáo', en: 'Report Reason', zh: '举报原因' },
flagPhotoSuccess: { vi: 'Đã báo cáo ảnh', en: 'Photo reported', zh: '已举报照片' },
flagPhotoError: { vi: 'Lỗi báo cáo ảnh', en: 'Error reporting photo', zh: '举报照片出错' },
inappropriate: { vi: 'Nội dung không phù hợp', en: 'Inappropriate content', zh: '不恰当的内容' },
spam: { vi: 'Spam', en: 'Spam', zh: '垃圾邮件' },
copyright: { vi: 'Vi phạm bản quyền', en: 'Copyright violation', zh: '侵犯版权' },
other: { vi: 'Khác', en: 'Other', zh: '其他' },
// Dashboard - Tours Management
toursManagement: { vi: 'Quản lý hành trình', en: 'Manage Tours', zh: '管理行程' },
toursManagementDesc: { vi: 'Xem và quản lý các chuyến đi', en: 'View and manage your travels', zh: '查看和管理您的旅行' },
// Dashboard - Notifications
notificationsEnabled: { vi: 'Đã bật thông báo', en: 'Notifications enabled', zh: '已启用通知' },
notificationsDisabled: { vi: 'Đã tắt thông báo', en: 'Notifications disabled', zh: '已禁用通知' },
emergencySharingEnabled: { vi: 'Đã bật chia sẻ hành trình cứu hộ.', en: 'Emergency sharing enabled.', zh: '已启用紧急分享。' },
emergencySharingDisabled: { vi: 'Đã tắt chia sẻ.', en: 'Sharing disabled.', zh: '已禁用分享。' },
// Dashboard - Connection Types
changeConnectionType: { vi: 'Mối quan hệ đã được chuyển sang nhóm: ', en: 'Relationship changed to group: ', zh: '关系已更改为组:' },
// Dashboard - Sections
settingsSection: { vi: 'Cài đặt', en: 'Settings', zh: '设置' },
manageRelations: { vi: 'Quản lý các mối quan hệ bạn bè, gia đình, duyệt các yêu cầu kết nối từ thành viên khác.', en: 'Manage friends, family relationships, and review connection requests from other members.', zh: '管理朋友和家人关系,审查来自其他成员的连接请求。' }
};
export const useTranslation = () => {