'use client'; import { createUser, updateUser, updateUserPassword, uploadAvatar } from '@/app/actions/data'; import { toast } from '@/hooks/use-toast'; import { serverSettingsAtom, usersAtom } from '@/lib/atoms'; import { useHelpers } from '@/lib/client-helpers'; import { Permission } from '@/lib/types'; import { passwordSchema, usernameSchema } from '@/lib/zod'; import { useAtom, useAtomValue } from 'jotai'; import _ from 'lodash'; import { User as UserIcon } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { useState } from 'react'; import { PermissionSelector } from './PermissionSelector'; import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Label } from './ui/label'; import { Switch } from './ui/switch'; interface UserFormProps { userId?: string; // if provided, we're editing; if not, we're creating onCancel: () => void; onSuccess: () => void; } export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps) { const t = useTranslations('UserForm'); const [users, setUsersData] = useAtom(usersAtom); const serverSettings = useAtomValue(serverSettingsAtom) const user = userId ? users.users.find(u => u.id === userId) : undefined; const { currentUser } = useHelpers() const getDefaultPermissions = (): Permission[] => [{ habit: { write: true, interact: true }, wishlist: { write: true, interact: true }, coins: { write: true, interact: true } }]; const [avatarPath, setAvatarPath] = useState(user?.avatarPath) const [username, setUsername] = useState(user?.username || ''); const [password, setPassword] = useState(''); const [disablePassword, setDisablePassword] = useState(user?.password === '' || serverSettings.isDemo); const [error, setError] = useState(''); const [avatarFile, setAvatarFile] = useState(null); const [isAdmin, setIsAdmin] = useState(user?.isAdmin || false); const [permissions, setPermissions] = useState( user?.permissions || getDefaultPermissions() ); const isEditing = !!user; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { // Validate username const usernameResult = usernameSchema.safeParse(username); if (!usernameResult.success) { setError(usernameResult.error.errors[0].message); return; } // Validate password unless disabled if (!disablePassword && password) { const passwordResult = passwordSchema.safeParse(password); if (!passwordResult.success) { setError(passwordResult.error.errors[0].message); return; } } if (isEditing) { // Update existing user if (username !== user.username || avatarPath !== user.avatarPath || !_.isEqual(permissions, user.permissions) || isAdmin !== user.isAdmin) { await updateUser(user.id, { username, avatarPath, permissions, isAdmin }); } // Handle password update if (disablePassword) { await updateUserPassword(user.id, undefined); } else if (password) { await updateUserPassword(user.id, password); } setUsersData(prev => ({ ...prev, users: prev.users.map(u => u.id === user.id ? { ...u, username, avatarPath, permissions, isAdmin, password: disablePassword ? '' : (password || u.password) // use the correct password to update atom } : u ), })); toast({ title: t('toastUserUpdatedTitle'), description: t('toastUserUpdatedDescription', { username }), variant: 'default' }); } else { // Create new user const formData = new FormData(); formData.append('username', username); if (disablePassword) { formData.append('password', ''); } else if (password) { formData.append('password', password); } formData.append('permissions', JSON.stringify(isAdmin ? undefined : permissions)); formData.append('isAdmin', JSON.stringify(isAdmin)); formData.append('avatarPath', avatarPath || ''); const newUser = await createUser(formData); setUsersData(prev => ({ ...prev, users: [...prev.users, newUser] })); toast({ title: t('toastUserCreatedTitle'), description: t('toastUserCreatedDescription', { username }), variant: 'default' }); } setPassword(''); setError(''); onSuccess(); } catch (err) { const action = isEditing ? t('actionUpdate') : t('actionCreate'); setError(err instanceof Error ? err.message : t('errorFailedUserAction', { action })); } }; const handleAvatarChange = async (file: File) => { if (file.size > 5 * 1024 * 1024) { // 5MB toast({ title: t('errorTitle'), description: t('errorFileSizeLimit'), variant: 'destructive' }); return; } const formData = new FormData(); formData.append('avatar', file); try { const path = await uploadAvatar(formData); setAvatarPath(path); setAvatarFile(null); // Clear the file since we've uploaded it toast({ title: t('toastAvatarUploadedTitle'), description: t('toastAvatarUploadedDescription'), variant: 'default' }); } catch (err) { toast({ title: t('errorTitle'), description: t('errorFailedAvatarUpload'), variant: 'destructive' }); } }; return (
{ const file = e.target.files?.[0]; if (file) { handleAvatarChange(file); } }} />
setUsername(e.target.value)} className={error ? 'border-red-500' : ''} />
setPassword(e.target.value)} className={error ? 'border-red-500' : ''} disabled={disablePassword} /> {serverSettings.isDemo && (

{t('demoPasswordDisabledMessage')}

)}
{error && (

{error}

)} {currentUser && currentUser.isAdmin && }
); }