feat: add city/state/country to complex, new settings page with sidebar, and Biome linting
- Added city, state, and country optional fields to Complex model - Updated onboarding to include optional location fields - Created new settings page with sidebar navigation (Datos del Complejo, Canchas) - Replaced ESLint with Biome for frontend and backend linting - Added parallel dev script with concurrently - Migrated register-routes to use direct app.route() pattern
This commit is contained in:
@@ -1,11 +1,5 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
import type { UserProfileResponse } from '@repo/api-contract'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -13,42 +7,51 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { apiClient } from '@/lib/api-client'
|
||||
import { useAuth } from '@/lib/auth'
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import type { UserProfileResponse } from '@repo/api-contract';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
const updateProfileSchema = z.object({
|
||||
fullName: z.string().min(1, 'El nombre es requerido'),
|
||||
})
|
||||
});
|
||||
|
||||
const updatePasswordSchema = z.object({
|
||||
currentPassword: z.string().min(1, 'La contraseña actual es requerida'),
|
||||
newPassword: z.string().min(6, 'La nueva contraseña debe tener al menos 6 caracteres'),
|
||||
confirmPassword: z.string().min(1, 'La confirmación de contraseña es requerida'),
|
||||
}).refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: 'Las contraseñas no coinciden',
|
||||
path: ['confirmPassword'],
|
||||
})
|
||||
const updatePasswordSchema = z
|
||||
.object({
|
||||
currentPassword: z.string().min(1, 'La contraseña actual es requerida'),
|
||||
newPassword: z.string().min(6, 'La nueva contraseña debe tener al menos 6 caracteres'),
|
||||
confirmPassword: z.string().min(1, 'La confirmación de contraseña es requerida'),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: 'Las contraseñas no coinciden',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
type UpdateProfileForm = z.infer<typeof updateProfileSchema>
|
||||
type UpdatePasswordForm = z.infer<typeof updatePasswordSchema>
|
||||
type UpdateProfileForm = z.infer<typeof updateProfileSchema>;
|
||||
type UpdatePasswordForm = z.infer<typeof updatePasswordSchema>;
|
||||
|
||||
export function ProfilePage() {
|
||||
const { user, displayName, initials, avatarUrl, isAuthenticated, updateProfile, updatePassword } = useAuth()
|
||||
const queryClient = useQueryClient()
|
||||
const { user, displayName, initials, avatarUrl, isAuthenticated, updateProfile, updatePassword } =
|
||||
useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const profileQuery = useQuery({
|
||||
queryKey: ['user-profile'],
|
||||
enabled: isAuthenticated,
|
||||
queryFn: (): Promise<UserProfileResponse> => apiClient.user.getProfile(),
|
||||
})
|
||||
});
|
||||
|
||||
const updateProfileForm = useForm<UpdateProfileForm>({
|
||||
resolver: zodResolver(updateProfileSchema),
|
||||
defaultValues: {
|
||||
fullName: displayName,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const updatePasswordForm = useForm<UpdatePasswordForm>({
|
||||
resolver: zodResolver(updatePasswordSchema),
|
||||
@@ -57,34 +60,34 @@ export function ProfilePage() {
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const updateProfileMutation = useMutation({
|
||||
mutationFn: updateProfile,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['user-profile'] })
|
||||
updateProfileForm.reset()
|
||||
queryClient.invalidateQueries({ queryKey: ['user-profile'] });
|
||||
updateProfileForm.reset();
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const updatePasswordMutation = useMutation({
|
||||
mutationFn: async (data: UpdatePasswordForm) => {
|
||||
// Note: Supabase doesn't require current password for password update
|
||||
// The user needs to be recently authenticated
|
||||
await updatePassword({ password: data.newPassword })
|
||||
await updatePassword({ password: data.newPassword });
|
||||
},
|
||||
onSuccess: () => {
|
||||
updatePasswordForm.reset()
|
||||
updatePasswordForm.reset();
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const onUpdateProfile = (data: UpdateProfileForm) => {
|
||||
updateProfileMutation.mutate(data)
|
||||
}
|
||||
updateProfileMutation.mutate(data);
|
||||
};
|
||||
|
||||
const onUpdatePassword = (data: UpdatePasswordForm) => {
|
||||
updatePasswordMutation.mutate(data)
|
||||
}
|
||||
updatePasswordMutation.mutate(data);
|
||||
};
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
@@ -99,7 +102,7 @@ export function ProfilePage() {
|
||||
</Button>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -122,9 +125,7 @@ export function ProfilePage() {
|
||||
{profileQuery.data?.email ?? user?.email}
|
||||
</p>
|
||||
{profileQuery.data?.role && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Rol: {profileQuery.data.role}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Rol: {profileQuery.data.role}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,10 +158,7 @@ export function ProfilePage() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={updateProfileMutation.isPending}
|
||||
>
|
||||
<Button type="submit" disabled={updateProfileMutation.isPending}>
|
||||
{updateProfileMutation.isPending ? 'Guardando...' : 'Guardar cambios'}
|
||||
</Button>
|
||||
</form>
|
||||
@@ -171,16 +169,17 @@ export function ProfilePage() {
|
||||
</p>
|
||||
)}
|
||||
{updateProfileMutation.isSuccess && (
|
||||
<p className="mt-4 text-sm text-green-600">
|
||||
Perfil actualizado exitosamente.
|
||||
</p>
|
||||
<p className="mt-4 text-sm text-green-600">Perfil actualizado exitosamente.</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border bg-card p-6 text-card-foreground shadow-sm">
|
||||
<h2 className="text-xl font-semibold mb-4">Cambiar Contraseña</h2>
|
||||
<Form {...updatePasswordForm}>
|
||||
<form onSubmit={updatePasswordForm.handleSubmit(onUpdatePassword)} className="space-y-4">
|
||||
<form
|
||||
onSubmit={updatePasswordForm.handleSubmit(onUpdatePassword)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={updatePasswordForm.control}
|
||||
name="currentPassword"
|
||||
@@ -220,10 +219,7 @@ export function ProfilePage() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={updatePasswordMutation.isPending}
|
||||
>
|
||||
<Button type="submit" disabled={updatePasswordMutation.isPending}>
|
||||
{updatePasswordMutation.isPending ? 'Cambiando...' : 'Cambiar contraseña'}
|
||||
</Button>
|
||||
</form>
|
||||
@@ -234,12 +230,10 @@ export function ProfilePage() {
|
||||
</p>
|
||||
)}
|
||||
{updatePasswordMutation.isSuccess && (
|
||||
<p className="mt-4 text-sm text-green-600">
|
||||
Contraseña cambiada exitosamente.
|
||||
</p>
|
||||
<p className="mt-4 text-sm text-green-600">Contraseña cambiada exitosamente.</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user