fix: tạo tags cho ảnh public của guest
This commit is contained in:
@@ -3,6 +3,8 @@ import { X, Send, MessageSquare, User, Loader2, Calendar, MapPin, Download, Edit
|
||||
import { io } from 'socket.io-client';
|
||||
import { CoordinateSelectModal } from './CoordinateSelectModal';
|
||||
import { useTranslation } from '../hooks/useTranslation';
|
||||
import { useConfirm } from '../hooks/useConfirm';
|
||||
import { useNotification } from '../hooks/useNotification';
|
||||
|
||||
interface Comment {
|
||||
id: string;
|
||||
@@ -48,6 +50,8 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
onUpdatePhoto
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const notify = useNotification();
|
||||
const [comments, setComments] = useState<Comment[]>([]);
|
||||
const [newComment, setNewComment] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -358,7 +362,12 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
};
|
||||
|
||||
const handleDeleteComment = async (commentId: string) => {
|
||||
if (!confirm(t('confirmDeleteComment') || 'Bạn có chắc chắn muốn xóa bình luận này không?')) return;
|
||||
const shouldDelete = await confirm({
|
||||
title: t('deleteComment') || 'Xóa bình luận',
|
||||
message: t('confirmDeleteComment') || 'Bạn có chắc chắn muốn xóa bình luận này không?'
|
||||
});
|
||||
if (!shouldDelete) return;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token') || localStorage.getItem('guest_token');
|
||||
const res = await fetch(`/api/v1/public-photos/comments/${commentId}`, {
|
||||
@@ -369,13 +378,14 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
});
|
||||
if (res.ok) {
|
||||
setComments(prev => prev.filter(c => c.id !== commentId));
|
||||
notify({ title: 'Thành công', message: 'Bình luận đã được xóa.', type: 'success' });
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert(err.message || 'Lỗi khi xóa bình luận.');
|
||||
notify({ title: 'Lỗi', message: err.message || 'Lỗi khi xóa bình luận.', type: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Lỗi khi xóa bình luận:', error);
|
||||
alert('Không thể kết nối đến máy chủ.');
|
||||
notify({ title: 'Lỗi', message: 'Không thể kết nối đến máy chủ.', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, Check, Plus, Trash2 } from 'lucide-react';
|
||||
import { useTranslation } from '../hooks/useTranslation';
|
||||
|
||||
interface TagSelectModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (tags: string[]) => void;
|
||||
photoUrl?: string;
|
||||
}
|
||||
|
||||
const AVAILABLE_TAGS = [
|
||||
{ id: 'phong-canh', label: '🏞️ Phong cảnh' },
|
||||
{ id: 'con-nguoi', label: '👥 Con người' },
|
||||
{ id: 'doi-thuong', label: '🎒 Đời thường' },
|
||||
{ id: 'bien', label: '🌊 Biển' },
|
||||
{ id: 'nui', label: '⛰️ Núi' },
|
||||
{ id: 'do-thi', label: '🏙️ Đô thị' },
|
||||
{ id: 'thuc-an', label: '🍜 Thức ăn' },
|
||||
{ id: 'cho', label: '🛍️ Chợ' },
|
||||
{ id: 'hien-dai', label: '🏗️ Hiện đại' },
|
||||
{ id: 'dong-vat', label: '🦁 Động vật' },
|
||||
{ id: 'thu-cung', label: '🐕 Thú cưng' }
|
||||
];
|
||||
|
||||
export const TagSelectModal: React.FC<TagSelectModalProps> = ({ isOpen, onClose, onConfirm, photoUrl }) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [customTagInput, setCustomTagInput] = useState('');
|
||||
const [customTags, setCustomTags] = useState<string[]>([]);
|
||||
|
||||
const toggleTag = (tagId: string) => {
|
||||
setSelectedTags(prev =>
|
||||
prev.includes(tagId)
|
||||
? prev.filter(t => t !== tagId)
|
||||
: [...prev, tagId]
|
||||
);
|
||||
};
|
||||
|
||||
const addCustomTag = () => {
|
||||
const trimmedTag = customTagInput.trim();
|
||||
if (trimmedTag && !customTags.includes(trimmedTag)) {
|
||||
setCustomTags(prev => [...prev, trimmedTag]);
|
||||
setCustomTagInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeCustomTag = (tag: string) => {
|
||||
setCustomTags(prev => prev.filter(t => t !== tag));
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
const allTags = [...selectedTags, ...customTags];
|
||||
onConfirm(allTags);
|
||||
setSelectedTags([]);
|
||||
setCustomTags([]);
|
||||
setCustomTagInput('');
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setSelectedTags([]);
|
||||
setCustomTags([]);
|
||||
setCustomTagInput('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[6000] flex items-center justify-center p-4">
|
||||
<div
|
||||
className="absolute inset-0 bg-slate-950/80 backdrop-blur-md"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
||||
<div className="relative bg-white dark:bg-slate-900 rounded-3xl shadow-2xl max-w-md w-full max-h-[90vh] overflow-y-auto animate-in zoom-in-95 duration-300">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-700 px-6 py-5 flex justify-between items-center">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
🏷️ Lựa chọn thẻ
|
||||
</h2>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-xl transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Image Preview */}
|
||||
{photoUrl && (
|
||||
<div className="flex justify-center">
|
||||
<img
|
||||
src={photoUrl}
|
||||
alt="Preview"
|
||||
className="max-w-full h-auto max-h-48 rounded-2xl shadow-lg object-cover border-2 border-gray-200 dark:border-slate-700"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Predefined Tags */}
|
||||
<div>
|
||||
<p className="text-xs font-black text-gray-600 dark:text-gray-400 mb-3 uppercase tracking-widest">Thẻ có sẵn</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{AVAILABLE_TAGS.map(tag => (
|
||||
<button
|
||||
key={tag.id}
|
||||
onClick={() => toggleTag(tag.id)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-xl transition-all text-xs font-bold border-2 ${
|
||||
selectedTags.includes(tag.id)
|
||||
? 'bg-blue-600 border-blue-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-slate-800 border-gray-200 dark:border-slate-700 text-gray-900 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
<span className="text-base">{tag.label.split(' ')[0]}</span>
|
||||
<span className="text-[10px]">{tag.label.substring(2)}</span>
|
||||
{selectedTags.includes(tag.id) && (
|
||||
<Check className="w-3 h-3 ml-auto" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Tag Input */}
|
||||
<div className="space-y-3 border-t border-gray-200 dark:border-slate-700 pt-4">
|
||||
<p className="text-xs font-black text-gray-600 dark:text-gray-400 uppercase tracking-widest">Thêm thẻ khác</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={customTagInput}
|
||||
onChange={(e) => setCustomTagInput(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
addCustomTag();
|
||||
}
|
||||
}}
|
||||
placeholder="Nhập thẻ mới..."
|
||||
className="flex-1 px-3 py-2 border-2 border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-gray-900 dark:text-white rounded-xl text-sm font-bold placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:border-blue-600"
|
||||
/>
|
||||
<button
|
||||
onClick={addCustomTag}
|
||||
className="px-3 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl font-bold transition-colors flex items-center gap-1 active:scale-95"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Custom Tags Display */}
|
||||
{customTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{customTags.map((tag, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="bg-emerald-100 dark:bg-emerald-950 text-emerald-700 dark:text-emerald-300 text-xs px-3 py-1.5 rounded-full font-semibold flex items-center gap-2 group"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
onClick={() => removeCustomTag(tag)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 hover:text-red-600" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* All Selected Tags Summary */}
|
||||
{(selectedTags.length > 0 || customTags.length > 0) && (
|
||||
<div className="pt-3 border-t border-gray-200 dark:border-slate-700">
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2 font-bold">
|
||||
✓ {selectedTags.length + customTags.length} thẻ đã chọn
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTags.map(tagId => {
|
||||
const tag = AVAILABLE_TAGS.find(t => t.id === tagId);
|
||||
return (
|
||||
<span
|
||||
key={tagId}
|
||||
className="bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300 text-xs px-3 py-1.5 rounded-full font-semibold"
|
||||
>
|
||||
{tag?.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{customTags.map((tag, idx) => (
|
||||
<span
|
||||
key={`custom-${idx}`}
|
||||
className="bg-emerald-100 dark:bg-emerald-950 text-emerald-700 dark:text-emerald-300 text-xs px-3 py-1.5 rounded-full font-semibold"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="sticky bottom-0 bg-white dark:bg-slate-900 border-t border-gray-200 dark:border-slate-700 px-6 py-4 flex gap-3">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="flex-1 px-4 py-3 rounded-xl border-2 border-gray-200 dark:border-slate-700 text-gray-900 dark:text-white font-bold hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
{t('cancel') || 'Hủy'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
className="flex-1 px-4 py-3 rounded-xl bg-blue-600 hover:bg-blue-700 text-white font-bold transition-colors shadow-md active:scale-95"
|
||||
>
|
||||
{t('confirm') || 'Xác nhận'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user