feat: tính năng chia sẻ khẩn cấp

This commit is contained in:
2026-06-21 12:52:47 +07:00
parent 8d79cd76f6
commit 403c169ddd
21 changed files with 2939 additions and 132 deletions
+120
View File
@@ -0,0 +1,120 @@
const loadScript = (src: string): Promise<void> => {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
resolve();
return;
}
const script = document.createElement('script');
script.src = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load script ${src}`));
document.head.appendChild(script);
});
};
const loadModerationLibraries = async () => {
// Load TensorFlow first
await loadScript('https://cdn.jsdelivr.net/npm/@tensorflow/tfjs');
// Load models after tfjs is available
await Promise.all([
loadScript('https://cdn.jsdelivr.net/npm/@tensorflow-models/blazeface'),
loadScript('https://cdn.jsdelivr.net/npm/nsfwjs@2.4.0/dist/bundle.js')
]);
};
export const processImageModeration = async (file: File): Promise<{ file: File; blocked: boolean }> => {
try {
const settingsRes = await fetch('/api/v1/moderation/settings');
if (!settingsRes.ok) return { file, blocked: false };
const settings = await settingsRes.json();
const { blockNsfw, blurFaces } = settings;
if (!blockNsfw && !blurFaces) {
return { file, blocked: false };
}
await loadModerationLibraries();
return new Promise((resolve) => {
const img = new Image();
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
resolve({ file, blocked: false });
return;
}
ctx.drawImage(img, 0, 0);
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;
}
} catch (e) {
console.error('NSFW validation error:', 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 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);
}
}
if (modified) {
canvas.toBlob((blob) => {
if (blob) {
const processedFile = new File([blob], file.name, { type: file.type });
resolve({ file: processedFile, blocked: false });
} else {
resolve({ file, blocked: false });
}
}, file.type);
} else {
resolve({ file, blocked: false });
}
};
img.onerror = () => {
resolve({ file, blocked: false });
};
img.src = URL.createObjectURL(file);
});
} catch (err) {
console.error('Image moderation failed:', err);
return { file, blocked: false };
}
};
+68
View File
@@ -0,0 +1,68 @@
import { useState, useEffect } from 'react';
export type Theme = 'light' | 'dark' | 'system';
export const useTheme = () => {
const [theme, setTheme] = useState<Theme>(() => {
return (localStorage.getItem('theme') as Theme) || 'system';
});
const applyTheme = (currentTheme: Theme) => {
const root = document.documentElement;
root.classList.remove('light', 'dark');
if (currentTheme === 'dark') {
root.classList.add('dark');
root.style.colorScheme = 'dark';
} else if (currentTheme === 'light') {
root.classList.add('light');
root.style.colorScheme = 'light';
} else {
// System
const systemIsDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (systemIsDark) {
root.classList.add('dark');
root.style.colorScheme = 'dark';
} else {
root.classList.add('light');
root.style.colorScheme = 'light';
}
}
};
const changeTheme = (newTheme: Theme) => {
localStorage.setItem('theme', newTheme);
setTheme(newTheme);
applyTheme(newTheme);
window.dispatchEvent(new Event('themeChange'));
};
useEffect(() => {
applyTheme(theme);
// Listen for system theme changes if theme is set to 'system'
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = () => {
if (localStorage.getItem('theme') === 'system' || !localStorage.getItem('theme')) {
applyTheme('system');
}
};
mediaQuery.addEventListener('change', handleSystemThemeChange);
const handleStorageChange = () => {
const storedTheme = (localStorage.getItem('theme') as Theme) || 'system';
setTheme(storedTheme);
applyTheme(storedTheme);
};
window.addEventListener('themeChange', handleStorageChange);
return () => {
mediaQuery.removeEventListener('change', handleSystemThemeChange);
window.removeEventListener('themeChange', handleStorageChange);
};
}, [theme]);
return { theme, changeTheme };
};
+147
View File
@@ -0,0 +1,147 @@
import { useState, useEffect } from 'react';
export type Language = 'vi' | 'en' | 'zh';
const translations: Record<string, Record<Language, string>> = {
// Common
appName: { vi: 'Travel Planner', en: 'Travel Planner', zh: '旅行规划' },
login: { vi: 'Đăng nhập', en: 'Log In', zh: '登录' },
signup: { vi: 'Đăng ký', en: 'Sign Up', zh: '注册' },
logout: { vi: 'Đăng xuất', en: 'Log Out', zh: '登出' },
cancel: { vi: 'Hủy', en: 'Cancel', zh: '取消' },
confirm: { vi: 'Xác nhận', en: 'Confirm', zh: '确认' },
save: { vi: 'Lưu', en: 'Save', zh: '保存' },
saving: { vi: 'Đang lưu...', en: 'Saving...', zh: '保存中...' },
loading: { vi: 'Đang tải...', en: 'Loading...', zh: '加载中...' },
success: { vi: 'Thành công', en: 'Success', zh: '成功' },
error: { vi: 'Lỗi', en: 'Error', zh: '错误' },
info: { vi: 'Thông tin', en: 'Info', zh: '信息' },
delete: { vi: 'Xóa', en: 'Delete', zh: '删除' },
edit: { vi: 'Chỉnh sửa', en: 'Edit', zh: '编辑' },
// Landing Page
welcomeBack: { vi: 'Chào mừng bạn quay trở lại!', en: 'Welcome back!', zh: '欢迎回来!' },
emailLabel: { vi: 'Email', en: 'Email', zh: '邮箱' },
passwordLabel: { vi: 'Mật khẩu', en: 'Password', zh: '密码' },
forgotPassword: { vi: 'Quên mật khẩu?', en: 'Forgot password?', zh: '忘记密码?' },
orLabel: { vi: 'Hoặc', en: 'Or', zh: '或' },
noAccount: { vi: 'Chưa có tài khoản?', en: "Don't have an account?", zh: '还没有账号?' },
createAccountNow: { vi: 'Tạo tài khoản ngay', en: 'Create one now', zh: '立即注册' },
quickCamera: { vi: 'Chụp ảnh nhanh', en: 'Quick Camera', zh: '快速相机' },
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: '探索旅行行程' },
// Explore Map
systemBtn: { vi: 'Hệ thống', en: 'System', zh: '系统管理' },
createTourBtn: { vi: 'Tạo Tour', en: 'Create Tour', zh: '创建行程' },
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: '坐标' },
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: '搜索地点...' },
// Itinerary Timeline
legLabel: { vi: 'Chặng', en: 'Leg', zh: '航段' },
legSequence: { vi: 'Chi tiết Chặng', en: 'Leg Details', zh: '航段详情' },
startDate: { vi: 'Bắt đầu', en: 'Start Date', zh: '开始日期' },
endDate: { vi: 'Kết thúc', en: 'End Date', zh: '结束日期' },
addLocation: { vi: 'Thêm địa điểm', en: 'Add Location', zh: '添加地点' },
dwellTime: { vi: 'Thời gian dừng', en: 'Dwell Time', zh: '停留时间' },
expenseLabel: { vi: 'Chi phí', en: 'Expense', zh: '费用' },
paidByLabel: { vi: 'Người chi trả', en: 'Paid By', zh: '付款人' },
optimizeBtn: { vi: 'Tối ưu', en: 'Optimize', zh: '优化' },
startPoint: { vi: 'Điểm bắt đầu', en: 'Start Point', zh: '起点' },
endPoint: { vi: 'Điểm kết thúc', en: 'End Point', zh: '终点' },
pinStartHelper: { vi: 'Nhấn để ghim điểm bắt đầu cho Tour...', en: 'Click to pin starting point...', zh: '点击锁定行程起点...' },
pinEndHelper: { vi: 'Nhấn để ghim điểm kết thúc cho Tour...', en: 'Click to pin ending point...', zh: '点击锁定行程终点...' },
noLegs: { vi: 'Chưa có chặng nào trong lộ trình.', en: 'No legs in the itinerary yet.', zh: '行程中暂无航段。' },
declareLegsBtn: { vi: 'Khai báo số chặng', en: 'Declare Leg Count', zh: '申报航段数' },
addSingleLeg: { vi: 'Thêm chặng lẻ vào cuối', en: 'Add leg to end', zh: '末尾添加单个航段' },
exportPDF: { vi: 'Xuất PDF', en: 'Export PDF', zh: '导出 PDF' },
// Tour Detail / Organizer Rating
rateOrganizer: { vi: 'Đánh giá ban tổ chức', en: 'Rate Organizer', zh: '评估组织者' },
honesty: { vi: 'Trung thực', en: 'Honesty', zh: '诚实度' },
transparency: { vi: 'Minh bạch', en: 'Transparency', zh: '透明度' },
enthusiasm: { vi: 'Nhiệt tình', en: 'Enthusiasm', zh: '热情度' },
cheerfulness: { vi: 'Vui vẻ', en: 'Cheerfulness', zh: '愉快度' },
seriousness: { vi: 'Nhiêm túc', en: 'Seriousness', zh: '认真度' },
planning: { vi: 'Có kế hoạch', en: 'Planning Skills', zh: '计划性' },
survival: { vi: 'Kỹ năng sinh tồn', en: 'Survival Skills', zh: '生存技能' },
rateTitle: { vi: 'Đánh giá Người tạo Tour', en: 'Rate Tour Creator', zh: '评价行程发起人' },
rateCommentPlaceholder: { vi: 'Nhập ý kiến đánh giá khác...', en: 'Enter other comments...', zh: '输入其他评价...' },
emergencyShare: { vi: 'Chia sẻ khẩn cấp', en: 'Emergency Share', zh: '紧急分享' },
emergencyShareTooltip: { vi: 'Bật chia sẻ để người thân có thể định vị bạn khi khẩn cấp', en: 'Enable sharing so family can locate you in emergencies', zh: '开启分享以便家人在紧急情况下定位您' },
copyShareLink: { vi: 'Sao chép liên kết chia sẻ', en: 'Copy share link', zh: '复制分享链接' },
// User Management / Moderation
tabUsers: { vi: 'Người dùng', en: 'Users', zh: '用户管理' },
tabPhotos: { vi: 'Ảnh công khai', en: 'Public Photos', zh: '公开照片' },
tabTrash: { vi: 'Ảnh rác', en: 'Trash Photos', zh: '垃圾照片' },
tabFilters: { vi: 'Bộ lọc', en: 'Filters', zh: '过滤器' },
filterNsfwToggle: { vi: 'Lọc hình ảnh khiêu dâm', en: 'Block NSFW Images', zh: '过滤淫秽图片' },
filterFaceBlurToggle: { vi: 'Làm mờ khuôn mặt', en: 'Automatic Face Blur', zh: '自动模糊人脸' },
wordFiltersTitle: { vi: 'Từ khóa cấm & Thay thế', en: 'Banned Words & Replacements', zh: '禁用词及替换词' },
addWordBtn: { vi: 'Thêm từ khóa', en: 'Add Word', zh: '添加词汇' },
wordLabel: { vi: 'Từ cấm', en: 'Banned Word', zh: '敏感词' },
replacementLabel: { vi: 'Từ thay thế', en: 'Replacement', zh: '替换词' },
commentSectionTitle: { vi: 'Bình luận cộng đồng', en: 'Community Comments', zh: '社区评论' },
// Dashboard / General Settings
myItineraries: { vi: 'Hành trình của tôi', en: 'My Itineraries', zh: '我的行程' },
chatMenu: { vi: 'Trò chuyện', en: 'Chat', zh: '聊天' },
friendsMenu: { vi: 'Danh sách bạn bè', en: 'Friends List', zh: '好友列表' },
muteNotifications: { vi: 'Tắt thông báo đẩy', en: 'Mute Notifications', zh: '关闭推送通知' },
unmuteNotifications: { vi: 'Bật thông báo đẩy', en: 'Unmute Notifications', zh: '开启推送通知' },
enterSecretKey: { vi: 'Nhập Admin Secret Key để mở khóa', en: 'Enter Admin Secret Key to unlock', zh: '输入管理员密钥解锁' },
invalidSecretKey: { vi: 'Mã Secret Key không hợp lệ', en: 'Invalid Secret Key', zh: '密钥无效' },
languageSelect: { vi: 'Ngôn ngữ', en: 'Language', zh: '语言' },
themeSelect: { vi: 'Giao diện', en: 'Theme', zh: '主题' },
themeLight: { vi: 'Sáng', en: 'Light', zh: '浅色' },
themeDark: { vi: 'Tối', en: 'Dark', zh: '深色' },
themeSystem: { vi: 'Hệ thống', en: 'System', zh: '跟随系统' },
// Emergency Share Journey Page
emergencyContacts: { vi: 'Liên hệ khẩn cấp', en: 'Emergency Contacts', zh: '紧急联系人' },
tourOwner: { vi: 'Người tạo Tour (Owner)', en: 'Tour Owner', zh: '发起人' },
tourManager: { vi: 'Người quản lý (Manager)', en: 'Tour Manager', zh: '管理员' },
phoneNumber: { vi: 'Số điện thoại', en: 'Phone Number', zh: '电话号码' },
emergencyJourney: { vi: 'Hành trình Cứu hộ Khẩn cấp', en: 'Emergency Rescue Journey', zh: '紧急救援行程' },
noPhone: { vi: 'Không có số điện thoại', en: 'No phone number', zh: '暂无电话' },
noStops: { vi: 'Chưa có điểm dừng nào', en: 'No stops declared', zh: '暂无停留点' },
viewMap: { vi: 'Bản đồ', en: 'Map', zh: '地图' },
viewTimeline: { vi: 'Lịch trình', en: 'Itinerary', zh: '行程表' },
sharedJourneyTitle: { vi: 'Hành trình chia sẻ khẩn cấp', en: 'Emergency Shared Journey', zh: '紧急分享行程' },
linkExpired: { vi: 'Liên kết không tồn tại hoặc đã bị vô hiệu hóa.', en: 'Link does not exist or has been disabled.', zh: '链接不存在或已被禁用。' }
};
export const useTranslation = () => {
const [lang, setLang] = useState<Language>(() => {
return (localStorage.getItem('language') as Language) || 'vi';
});
const changeLanguage = (newLang: Language) => {
localStorage.setItem('language', newLang);
setLang(newLang);
// Dispatch custom event to sync across components
window.dispatchEvent(new Event('languageChange'));
};
useEffect(() => {
const handleLangChange = () => {
setLang((localStorage.getItem('language') as Language) || 'vi');
};
window.addEventListener('languageChange', handleLangChange);
return () => window.removeEventListener('languageChange', handleLangChange);
}, []);
const t = (key: string): string => {
if (!translations[key]) {
return key;
}
return translations[key][lang];
};
return { t, lang, changeLanguage };
};