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
+210 -1
View File
@@ -7,10 +7,14 @@ interface UserManagementModalProps {
}
export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen, onClose }) => {
const [activeTab, setActiveTab] = useState<'users' | 'photos' | 'trash'>('users');
const [activeTab, setActiveTab] = useState<'users' | 'photos' | 'trash' | 'filters'>('users');
const [users, setUsers] = useState<any[]>([]);
const [photos, setPhotos] = useState<any[]>([]);
const [trashPhotos, setTrashPhotos] = useState<any[]>([]);
const [moderationSetting, setModerationSetting] = useState({ blockNsfw: false, blurFaces: false });
const [wordFilters, setWordFilters] = useState<any[]>([]);
const [newWord, setNewWord] = useState('');
const [newReplacement, setNewReplacement] = useState('');
const [loading, setLoading] = useState(false);
const [photosLoading, setPhotosLoading] = useState(false);
const [trashLoading, setTrashLoading] = useState(false);
@@ -105,6 +109,84 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
}
};
const fetchModerationSettings = async () => {
try {
const res = await fetch('/api/v1/moderation/settings');
if (res.ok) {
const data = await res.json();
setModerationSetting({ blockNsfw: data.blockNsfw, blurFaces: data.blurFaces });
}
} catch (e) {
console.error(e);
}
};
const fetchWordFilters = async () => {
try {
const res = await fetch('/api/v1/admin/moderation/word-filters', {
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
});
if (res.ok) {
const data = await res.json();
setWordFilters(data);
}
} catch (e) {
console.error(e);
}
};
const handleUpdateModeration = async (field: 'blockNsfw' | 'blurFaces', value: boolean) => {
const updated = { ...moderationSetting, [field]: value };
setModerationSetting(updated);
try {
await fetch('/api/v1/admin/moderation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(updated)
});
} catch (e) {
console.error(e);
}
};
const handleAddWordFilter = async () => {
if (!newWord.trim()) return;
try {
const res = await fetch('/api/v1/admin/moderation/word-filters', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ word: newWord, replacement: newReplacement })
});
if (res.ok) {
setNewWord('');
setNewReplacement('');
fetchWordFilters();
}
} catch (e) {
console.error(e);
}
};
const handleDeleteWordFilter = async (id: string) => {
try {
const res = await fetch(`/api/v1/admin/moderation/word-filters/${id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
});
if (res.ok) {
fetchWordFilters();
}
} catch (e) {
console.error(e);
}
};
useEffect(() => {
if (isOpen) {
if (activeTab === 'users') {
@@ -113,6 +195,9 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
fetchPhotos();
} else if (activeTab === 'trash') {
fetchTrashPhotos();
} else if (activeTab === 'filters') {
fetchModerationSettings();
fetchWordFilters();
}
}
}, [isOpen, activeTab]);
@@ -212,6 +297,15 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
<Trash2 className="w-4 h-4" />
nh rác
</button>
<button
onClick={() => setActiveTab('filters')}
className={`py-4 px-4 font-bold text-sm transition-all border-b-2 -mb-[2px] flex items-center gap-2 ${
activeTab === 'filters' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-400 hover:text-gray-600'
}`}
>
<Shield className="w-4 h-4" />
Bộ lọc
</button>
</div>
{/* Content Body */}
@@ -409,6 +503,121 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
</div>
)
)}
{activeTab === 'filters' && (
<div className="space-y-6 text-gray-800">
<div className="bg-gray-50/50 p-6 rounded-2xl border border-gray-100/80 space-y-4 text-left">
<h3 className="text-sm font-black uppercase text-blue-600 tracking-wider flex items-center gap-1.5">
Cấu hình kiểm duyệt hình nh
</h3>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-4 bg-white rounded-xl border border-gray-100 shadow-sm">
<div className="flex flex-col text-left">
<span className="text-sm font-bold text-gray-800">Lọc hình nh khiêu dâm (NSFW)</span>
<span className="text-xs text-gray-400 mt-0.5">Tự đng phát hiện chặn tải lên các hình nh nội dung người lớn nhạy cảm.</span>
</div>
<button
onClick={() => handleUpdateModeration('blockNsfw', !moderationSetting.blockNsfw)}
className={`w-14 h-8 rounded-full transition-all relative p-1 cursor-pointer shrink-0 ${
moderationSetting.blockNsfw ? 'bg-blue-600' : 'bg-gray-200'
}`}
>
<div className={`w-6 h-6 bg-white rounded-full shadow-md transition-all absolute top-1 ${
moderationSetting.blockNsfw ? 'right-1' : 'left-1'
}`} />
</button>
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-4 bg-white rounded-xl border border-gray-100 shadow-sm">
<div className="flex flex-col text-left">
<span className="text-sm font-bold text-gray-800">Tự đng làm mờ khuôn mặt</span>
<span className="text-xs text-gray-400 mt-0.5">Tự đng nhận diện khuôn mặt người trong nh đ làm mờ bảo mật trước khi lưu trữ.</span>
</div>
<button
onClick={() => handleUpdateModeration('blurFaces', !moderationSetting.blurFaces)}
className={`w-14 h-8 rounded-full transition-all relative p-1 cursor-pointer shrink-0 ${
moderationSetting.blurFaces ? 'bg-blue-600' : 'bg-gray-200'
}`}
>
<div className={`w-6 h-6 bg-white rounded-full shadow-md transition-all absolute top-1 ${
moderationSetting.blurFaces ? 'right-1' : 'left-1'
}`} />
</button>
</div>
</div>
<div className="bg-gray-50/50 p-6 rounded-2xl border border-gray-100/80 space-y-4 text-left">
<h3 className="text-sm font-black uppercase text-blue-600 tracking-wider flex items-center gap-1.5">
📝 Cấu hình bộ lọc văn bản
</h3>
{/* Add Word Filter Form */}
<div className="flex flex-col sm:flex-row gap-3 bg-white p-4 rounded-xl border border-gray-100 shadow-sm">
<div className="flex-1">
<label className="block text-[10px] uppercase font-black text-gray-400 mb-1">Từ cấm</label>
<input
type="text"
placeholder="Nhập từ cấm..."
value={newWord}
onChange={(e) => setNewWord(e.target.value)}
className="w-full px-4 py-2 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50/20"
/>
</div>
<div className="flex-1">
<label className="block text-[10px] uppercase font-black text-gray-400 mb-1">Từ thay thế</label>
<input
type="text"
placeholder="Nhập từ thay thế..."
value={newReplacement}
onChange={(e) => setNewReplacement(e.target.value)}
className="w-full px-4 py-2 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50/20"
/>
</div>
<div className="flex items-end">
<button
onClick={handleAddWordFilter}
className="w-full sm:w-auto px-5 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded-xl text-xs font-bold transition-all shadow-md active:scale-95 cursor-pointer h-[38px] flex items-center justify-center shrink-0"
>
Thêm bộ lọc
</button>
</div>
</div>
{/* Word Filter List */}
{wordFilters.length === 0 ? (
<div className="text-center py-8 text-gray-400 italic text-xs">Chưa cấu hình bộ lọc từ khóa nào.</div>
) : (
<div className="border border-gray-100 rounded-2xl overflow-hidden shadow-sm bg-white">
<table className="w-full text-left border-collapse">
<thead>
<tr className="text-gray-400 text-xs uppercase border-b border-gray-100 bg-gray-50/30">
<th className="py-2.5 px-4 font-bold">Từ cấm</th>
<th className="py-2.5 px-4 font-bold">Từ thay thế</th>
<th className="py-2.5 px-4 font-bold text-right">Thao tác</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{wordFilters.map((wf) => (
<tr key={wf.id} className="hover:bg-gray-50/30">
<td className="py-2.5 px-4 text-xs font-bold text-red-500">{wf.word}</td>
<td className="py-2.5 px-4 text-xs font-semibold text-green-600">{wf.replacement}</td>
<td className="py-2.5 px-4 text-right">
<button
onClick={() => handleDeleteWordFilter(wf.id)}
className="p-1.5 text-gray-400 hover:text-red-500 rounded-lg hover:bg-red-50 transition-colors cursor-pointer"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>