feat: tính năng chia sẻ khẩn cấp
This commit is contained in:
@@ -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 và chặn tải lên các hình ảnh có 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>
|
||||
|
||||
Reference in New Issue
Block a user