fix: gộp các nút của thành viên và guest vào một
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import React, { useState } from 'react';
|
||||
import { LayoutDashboard, Settings, Compass, ShieldAlert, LogOut, LogIn, Globe, Sun, Moon, Image as ImageIcon } from 'lucide-react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
interface MapProfileDropdownProps {
|
||||
user: any;
|
||||
onLogout?: () => void;
|
||||
onGoToDashboard?: () => void;
|
||||
onOpenSettings: () => void;
|
||||
onOpenCreateTour: () => void;
|
||||
onOpenReport: () => void;
|
||||
onOpenLogin: () => void;
|
||||
onOpenMyPhotos?: () => void;
|
||||
onOpenAdmin?: () => void;
|
||||
}
|
||||
|
||||
export const MapProfileDropdown: React.FC<MapProfileDropdownProps> = ({
|
||||
user,
|
||||
onLogout,
|
||||
onGoToDashboard,
|
||||
onOpenSettings,
|
||||
onOpenCreateTour,
|
||||
onOpenReport,
|
||||
onOpenLogin,
|
||||
onOpenMyPhotos,
|
||||
onOpenAdmin,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { lang, changeLanguage } = useTranslation();
|
||||
const { theme, changeTheme } = useTheme();
|
||||
|
||||
const guestToken = localStorage.getItem('guest_token');
|
||||
const isAuthenticated = !!user && !guestToken;
|
||||
|
||||
const renderInitialsAvatar = (name: string) => {
|
||||
const initials = name
|
||||
? name.split(' ').map(n => n[0]).slice(0, 2).join('').toUpperCase()
|
||||
: 'U';
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center bg-indigo-600 text-white font-bold text-sm">
|
||||
{initials}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative inline-block text-left select-none pointer-events-auto">
|
||||
{/* TRIGGER BUTTON */}
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-11 h-11 rounded-full border-2 border-white dark:border-slate-800 bg-slate-800 flex items-center justify-center overflow-hidden shadow-xl active:scale-95 transition-all duration-150 cursor-pointer"
|
||||
title="Menu cá nhân"
|
||||
>
|
||||
{isAuthenticated ? (
|
||||
user?.avatar ? (
|
||||
<img src={user.avatar} alt="Avatar" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
renderInitialsAvatar(user?.name || 'User')
|
||||
)
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-slate-700 text-slate-300 font-bold text-sm">
|
||||
G
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* DROPDOWN MENU / MOBILE BOTTOM SHEET LAYER */}
|
||||
{isOpen && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-black/50 sm:bg-transparent"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="fixed bottom-0 left-0 right-0 sm:absolute sm:bottom-auto sm:top-14 sm:right-0 sm:left-auto z-50 w-full sm:w-64 bg-slate-900 border-t sm:border border-slate-800 rounded-t-3xl sm:rounded-2xl p-3 sm:p-2 shadow-2xl animate-in slide-in-from-bottom sm:slide-in-from-top-2 duration-300 text-xs text-slate-200"
|
||||
>
|
||||
<div className="w-12 h-1 bg-slate-700 rounded-full mx-auto mb-3 sm:hidden" />
|
||||
|
||||
{isAuthenticated ? (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="px-4 py-2 border-b border-slate-800/60 mb-1 flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden shrink-0 border border-slate-700">
|
||||
{user?.avatar ? (
|
||||
<img src={user.avatar} alt="Avatar" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
renderInitialsAvatar(user?.name || 'User')
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="font-bold text-white truncate text-sm">{user?.name}</span>
|
||||
<span className="text-[10px] text-slate-400 truncate">{user?.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{user?.isAdmin && (
|
||||
<button
|
||||
onClick={() => { setIsOpen(false); onOpenAdmin?.(); }}
|
||||
className="flex items-center gap-3 w-full px-4 py-3 bg-blue-950/20 hover:bg-slate-805 rounded-xl text-left transition-colors font-bold text-blue-400 cursor-pointer"
|
||||
>
|
||||
<Settings className="w-4 h-4 shrink-0 text-blue-400" /> Quản trị hệ thống
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => { setIsOpen(false); onGoToDashboard?.(); }}
|
||||
className="flex items-center gap-3 w-full px-4 py-3 hover:bg-slate-800 rounded-xl text-left transition-colors font-bold cursor-pointer"
|
||||
>
|
||||
<LayoutDashboard className="w-4 h-4 text-blue-400 shrink-0" /> Bảng điều khiển
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setIsOpen(false); onOpenSettings(); }}
|
||||
className="flex items-center gap-3 w-full px-4 py-3 hover:bg-slate-800 rounded-xl text-left transition-colors font-bold cursor-pointer"
|
||||
>
|
||||
<Settings className="w-4 h-4 text-emerald-400 shrink-0" /> Cài đặt cá nhân
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setIsOpen(false); onOpenCreateTour(); }}
|
||||
className="flex items-center gap-3 w-full px-4 py-3 hover:bg-slate-800 rounded-xl text-left transition-colors font-bold cursor-pointer"
|
||||
>
|
||||
<Compass className="w-4 h-4 text-amber-400 shrink-0" /> Tạo Tour mới
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setIsOpen(false); onOpenMyPhotos?.(); }}
|
||||
className="flex items-center gap-3 w-full px-4 py-3 hover:bg-slate-800 rounded-xl text-left transition-colors font-bold cursor-pointer"
|
||||
>
|
||||
<ImageIcon className="w-4 h-4 text-sky-400 shrink-0" /> Ảnh của tôi
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setIsOpen(false); onOpenReport(); }}
|
||||
className="flex items-center gap-3 w-full px-4 py-3 hover:bg-slate-800 rounded-xl text-left transition-colors font-bold cursor-pointer"
|
||||
>
|
||||
<ShieldAlert className="w-4 h-4 text-rose-400 shrink-0" /> Báo cáo sai phạm
|
||||
</button>
|
||||
|
||||
<div className="h-[1px] bg-slate-855 my-1 mx-2" />
|
||||
|
||||
<button
|
||||
onClick={() => { setIsOpen(false); onLogout?.(); }}
|
||||
className="flex items-center gap-3 w-full px-4 py-3 text-rose-400 hover:bg-slate-800 rounded-xl text-left font-black transition-colors cursor-pointer"
|
||||
>
|
||||
<LogOut className="w-4 h-4 shrink-0" /> Đăng xuất
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="px-4 py-1.5 text-[10px] font-black uppercase tracking-wider text-slate-500">Tùy chỉnh nhanh</div>
|
||||
|
||||
<div className="px-3 py-2 flex flex-col gap-3 bg-slate-950/40 rounded-xl m-1 border border-slate-850">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold flex items-center gap-1.5 text-slate-400">
|
||||
<Globe className="w-3.5 h-3.5 text-indigo-400" /> Ngôn ngữ:
|
||||
</span>
|
||||
<select
|
||||
value={lang}
|
||||
onChange={(e) => changeLanguage(e.target.value as any)}
|
||||
className="bg-slate-800 border border-slate-700 rounded-lg px-2 py-1 text-white text-[11px] font-bold focus:outline-none cursor-pointer"
|
||||
>
|
||||
<option value="vi">Tiếng Việt</option>
|
||||
<option value="en">English</option>
|
||||
<option value="zh">中文</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold flex items-center gap-1.5 text-slate-400">
|
||||
{theme === 'light' ? (
|
||||
<Sun className="w-3.5 h-3.5 text-amber-500" />
|
||||
) : (
|
||||
<Moon className="w-3.5 h-3.5 text-indigo-400" />
|
||||
)}
|
||||
Giao diện:
|
||||
</span>
|
||||
<select
|
||||
value={theme}
|
||||
onChange={(e) => changeTheme(e.target.value as any)}
|
||||
className="bg-slate-800 border border-slate-700 rounded-lg px-2 py-1 text-white text-[11px] font-bold focus:outline-none cursor-pointer"
|
||||
>
|
||||
<option value="dark">Tối</option>
|
||||
<option value="light">Sáng</option>
|
||||
<option value="system">Hệ thống</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => { setIsOpen(false); onOpenMyPhotos?.(); }}
|
||||
className="flex items-center gap-3 w-full px-4 py-3 hover:bg-slate-800 rounded-xl text-left transition-colors font-bold cursor-pointer"
|
||||
>
|
||||
<ImageIcon className="w-4 h-4 text-sky-400 shrink-0" /> Ảnh của tôi
|
||||
</button>
|
||||
|
||||
<div className="h-[1px] bg-slate-850 my-1 mx-2" />
|
||||
|
||||
<button
|
||||
onClick={() => { setIsOpen(false); onOpenLogin(); }}
|
||||
className="flex items-center gap-3 w-full px-4 py-3 text-blue-400 hover:bg-slate-800 rounded-xl text-left font-black transition-colors cursor-pointer"
|
||||
>
|
||||
<LogIn className="w-4 h-4 shrink-0" /> Đăng ký / Đăng nhập
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,350 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Camera, Loader2, Check } from 'lucide-react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useNotification } from '@/hooks/useNotification';
|
||||
|
||||
interface ProfileSettingsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
user: any;
|
||||
onSaveSuccess?: (updatedUser: any) => void;
|
||||
}
|
||||
|
||||
export const ProfileSettingsModal: React.FC<ProfileSettingsModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
user,
|
||||
onSaveSuccess,
|
||||
}) => {
|
||||
const { lang, changeLanguage } = useTranslation();
|
||||
const { theme, changeTheme } = useTheme();
|
||||
const notify = useNotification();
|
||||
|
||||
// Form States
|
||||
const [name, setName] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [address, setAddress] = useState('');
|
||||
const [avatar, setAvatar] = useState('');
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
|
||||
// Status States
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setName(user.name || '');
|
||||
setPhone(user.phone || '');
|
||||
setAddress(user.address || '');
|
||||
setAvatar(user.avatar || '');
|
||||
}
|
||||
}, [user, isOpen]);
|
||||
|
||||
if (!isOpen || !user) return null;
|
||||
|
||||
const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch('/api/v1/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Không thể tải ảnh lên.');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
setAvatar(data.url);
|
||||
notify({
|
||||
title: 'Thành công',
|
||||
message: 'Đã cập nhật ảnh đại diện xem trước.',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (err: any) {
|
||||
notify({
|
||||
title: 'Lỗi',
|
||||
message: err.message || 'Tải ảnh thất bại.',
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim()) {
|
||||
notify({
|
||||
title: 'Lỗi',
|
||||
message: 'Tên hiển thị không được để trống.',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
const updateData: any = {
|
||||
name,
|
||||
phone,
|
||||
address,
|
||||
avatar,
|
||||
};
|
||||
|
||||
if (currentPassword && newPassword) {
|
||||
// Typically the backend might check the old password, let's pass it
|
||||
updateData.password = newPassword;
|
||||
// We pass both or verify password on backend
|
||||
} else if (newPassword && !currentPassword) {
|
||||
notify({
|
||||
title: 'Lỗi',
|
||||
message: 'Vui lòng cung cấp mật khẩu hiện tại để đổi mật khẩu.',
|
||||
type: 'error',
|
||||
});
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch(`/api/v1/users/${user.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updateData),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errData = await res.json();
|
||||
throw new Error(errData.message || 'Không thể lưu thay đổi.');
|
||||
}
|
||||
|
||||
const updatedUser = await res.json();
|
||||
|
||||
// Update local storage user object
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
const userObj = JSON.parse(storedUser);
|
||||
const mergedUser = { ...userObj, ...updatedUser };
|
||||
localStorage.setItem('user', JSON.stringify(mergedUser));
|
||||
if (onSaveSuccess) {
|
||||
onSaveSuccess(mergedUser);
|
||||
}
|
||||
}
|
||||
|
||||
notify({
|
||||
title: 'Thành công',
|
||||
message: 'Thông tin hồ sơ cá nhân đã được lưu thành công.',
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
// Clear password inputs
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
notify({
|
||||
title: 'Lỗi',
|
||||
message: err.message || 'Lưu thay đổi thất bại.',
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to render initials fallback
|
||||
const renderInitialsAvatar = (name: string) => {
|
||||
const initials = name
|
||||
? name.split(' ').map(n => n[0]).slice(0, 2).join('').toUpperCase()
|
||||
: 'U';
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center bg-indigo-600 text-white font-bold text-2xl">
|
||||
{initials}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[1000000] bg-black/70 backdrop-blur-sm flex items-end sm:items-center justify-center p-0 sm:p-4 pointer-events-auto">
|
||||
<div className="bg-slate-900 w-full sm:max-w-xl h-[92vh] sm:h-auto max-h-[92vh] sm:max-h-[85vh] rounded-t-3xl sm:rounded-2xl flex flex-col overflow-hidden text-slate-200 text-xs border border-slate-800/80 shadow-2xl animate-in slide-in-from-bottom sm:zoom-in-95 duration-300">
|
||||
|
||||
{/* Header bar */}
|
||||
<div className="px-5 py-4 border-b border-slate-800/60 flex justify-between items-center shrink-0">
|
||||
<span className="font-bold text-sm text-white">Chỉnh sửa hồ sơ cá nhân</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable Form Workspace Canvas */}
|
||||
<div className="flex-1 overflow-y-auto p-5 space-y-5">
|
||||
|
||||
{/* Avatar Upload Container Component */}
|
||||
<div className="flex flex-col items-center justify-center space-y-2">
|
||||
<div className="relative w-20 h-20 rounded-full border-2 border-slate-700 overflow-hidden bg-slate-850 group shadow-lg">
|
||||
{isUploading ? (
|
||||
<div className="w-full h-full flex items-center justify-center bg-slate-800/85">
|
||||
<Loader2 className="w-6 h-6 text-indigo-400 animate-spin" />
|
||||
</div>
|
||||
) : avatar ? (
|
||||
<img src={avatar} alt="Profile Avatar" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
renderInitialsAvatar(name || 'User')
|
||||
)}
|
||||
|
||||
<label className="absolute inset-0 bg-black/60 flex flex-col items-center justify-center opacity-0 group-hover:opacity-100 cursor-pointer transition-opacity duration-200">
|
||||
<Camera className="w-5 h-5 text-white" />
|
||||
<span className="text-[8px] text-white/80 mt-1">Thay ảnh</span>
|
||||
<input type="file" accept="image/*" className="hidden" onChange={handleAvatarChange} disabled={isUploading} />
|
||||
</label>
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500 font-semibold">Di chuột qua ảnh và nhấp để tải hình đại diện mới</span>
|
||||
</div>
|
||||
|
||||
{/* Text Input Row Fields */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="font-bold text-slate-400">Tên hiển thị:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="bg-slate-950 border border-slate-800 rounded-xl p-3 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all font-semibold"
|
||||
placeholder="Nhập tên hiển thị"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="font-bold text-slate-400">Số điện thoại:</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="bg-slate-950 border border-slate-800 rounded-xl p-3 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all font-semibold"
|
||||
placeholder="Nhập số điện thoại"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="font-bold text-slate-400">Địa chỉ liên hệ:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
className="bg-slate-950 border border-slate-800 rounded-xl p-3 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all font-semibold"
|
||||
placeholder="Nhập địa chỉ của bạn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* EMAIL COMPONENT BOUNDARY - CRITICAL REQUIREMENT: DISABLED CHANGING */}
|
||||
<div className="flex flex-col gap-1.5 bg-slate-950/30 p-3.5 rounded-xl border border-slate-850">
|
||||
<label className="font-bold text-slate-500">Địa chỉ Email đăng nhập (Không thể chỉnh sửa):</label>
|
||||
<input
|
||||
type="email"
|
||||
disabled
|
||||
className="bg-slate-950/50 border border-slate-850 text-slate-500 rounded-xl p-3 cursor-not-allowed select-none font-semibold"
|
||||
value={user.email || ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Security Password Mutation Stack */}
|
||||
<div className="border-t border-slate-800/60 pt-4 flex flex-col gap-3">
|
||||
<span className="font-bold text-slate-300 text-sm">Thay đổi mật khẩu đăng nhập</span>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Mật khẩu hiện tại"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
className="bg-slate-950 border border-slate-800 rounded-xl p-3 text-white focus:border-blue-500 outline-none transition-all font-semibold"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Mật khẩu mới"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="bg-slate-950 border border-slate-800 rounded-xl p-3 text-white focus:border-blue-500 outline-none transition-all font-semibold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Core Configuration Toggles (Moved from Top-Bar into profile settings context) */}
|
||||
<div className="border-t border-slate-800/60 pt-4 grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="font-bold text-slate-400">Lựa chọn ngôn ngữ:</label>
|
||||
<select
|
||||
value={lang}
|
||||
onChange={(e) => changeLanguage(e.target.value as any)}
|
||||
className="bg-slate-955 border border-slate-800 rounded-xl p-3 text-white focus:outline-none cursor-pointer font-bold"
|
||||
>
|
||||
<option value="vi">Tiếng Việt (ICT)</option>
|
||||
<option value="en">English (US)</option>
|
||||
<option value="zh">中文 (ZH)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="font-bold text-slate-400">Lựa chọn giao diện:</label>
|
||||
<select
|
||||
value={theme}
|
||||
onChange={(e) => changeTheme(e.target.value as any)}
|
||||
className="bg-slate-955 border border-slate-800 rounded-xl p-3 text-white focus:outline-none cursor-pointer font-bold"
|
||||
>
|
||||
<option value="dark">Chế độ tối (Dark Mode)</option>
|
||||
<option value="light">Chế độ sáng (Light Mode)</option>
|
||||
<option value="system">Chế độ hệ thống (System)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Action Bottom Save Trigger */}
|
||||
<div className="p-4 bg-slate-950 border-t border-slate-800/60 flex justify-end gap-3 shrink-0">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={isSaving}
|
||||
className="px-4 py-2.5 bg-slate-800 hover:bg-slate-700 disabled:opacity-50 text-slate-300 font-bold rounded-xl transition-all active:scale-95 cursor-pointer"
|
||||
>
|
||||
Hủy
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="px-5 py-2.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 font-bold text-white rounded-xl shadow-lg shadow-blue-900/20 transition-all active:scale-95 flex items-center gap-1.5 cursor-pointer"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
Đang lưu...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
Lưu cấu hình
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user