Added 'No show' status. Edit user profile
This commit is contained in:
@@ -12,6 +12,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@repo/api-contract": "workspace:*",
|
"@repo/api-contract": "workspace:*",
|
||||||
"@supabase/supabase-js": "^2.101.1",
|
"@supabase/supabase-js": "^2.101.1",
|
||||||
"@tanstack/react-query": "^5.96.1",
|
"@tanstack/react-query": "^5.96.1",
|
||||||
|
|||||||
174
apps/frontend/src/components/ui/form.tsx
Normal file
174
apps/frontend/src/components/ui/form.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import type {
|
||||||
|
ControllerProps,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
} from "react-hook-form"
|
||||||
|
import { Controller, FormProvider, useFormContext } from "react-hook-form"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
|
||||||
|
const Form = FormProvider
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
> = {
|
||||||
|
name: TName
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext)
|
||||||
|
const itemContext = React.useContext(FormItemContext)
|
||||||
|
const { getFieldState, formState } = useFormContext()
|
||||||
|
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState)
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const id = React.useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormItem.displayName = "FormItem"
|
||||||
|
|
||||||
|
const FormLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(error && "text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormLabel.displayName = "FormLabel"
|
||||||
|
|
||||||
|
const FormControl = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Slot>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
|
>(({ ...props }, ref) => {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
ref={ref}
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormControl.displayName = "FormControl"
|
||||||
|
|
||||||
|
const FormDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormDescription.displayName = "FormDescription"
|
||||||
|
|
||||||
|
const FormMessage = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, children, ...props }, ref) => {
|
||||||
|
const { error, formMessageId } = useFormField()
|
||||||
|
const body = error ? String(error?.message) : children
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-sm font-medium text-destructive", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormMessage.displayName = "FormMessage"
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormMessage,
|
||||||
|
}
|
||||||
@@ -128,7 +128,7 @@ export function CourtFormSection({
|
|||||||
const buttonText = editingCourt ? 'Guardar cambios' : 'Agregar cancha'
|
const buttonText = editingCourt ? 'Guardar cambios' : 'Agregar cancha'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-xl border bg-card p-4 text-card-foreground shadow-sm sm:p-5">
|
<section className="rounded-xl border bg-card p-4 text-card-foreground shadow-sm sm:p-5 mb-4">
|
||||||
<h3 className="text-lg font-semibold">{title}</h3>
|
<h3 className="text-lg font-semibold">{title}</h3>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Configura nombre, deporte, duración, precio base y horarios. El backend ya
|
Configura nombre, deporte, duración, precio base y horarios. El backend ya
|
||||||
|
|||||||
@@ -63,13 +63,33 @@ function formatDateLabel(dateIso: string) {
|
|||||||
}).format(date)
|
}).format(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusLabel(status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED') {
|
function getEffectiveStatus(booking: AdminBooking): 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NO_SHOW' {
|
||||||
|
if (booking.status !== 'CONFIRMED') {
|
||||||
|
return booking.status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if booking is past end time and still confirmed
|
||||||
|
const now = new Date()
|
||||||
|
const bookingDateTime = new Date(`${booking.date}T${booking.endTime}:00`)
|
||||||
|
|
||||||
|
if (now > bookingDateTime) {
|
||||||
|
return 'NO_SHOW'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'CONFIRMED'
|
||||||
|
}
|
||||||
|
|
||||||
|
function effectiveStatusLabel(status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NO_SHOW') {
|
||||||
|
if (status === 'NO_SHOW') return 'No show'
|
||||||
if (status === 'COMPLETED') return 'Cumplida'
|
if (status === 'COMPLETED') return 'Cumplida'
|
||||||
if (status === 'CANCELLED') return 'Cancelada'
|
if (status === 'CANCELLED') return 'Cancelada'
|
||||||
return 'Confirmada'
|
return 'Confirmada'
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusClassName(status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED') {
|
function effectiveStatusClassName(status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NO_SHOW') {
|
||||||
|
if (status === 'NO_SHOW') {
|
||||||
|
return 'border-orange-200 bg-orange-50 text-orange-700'
|
||||||
|
}
|
||||||
if (status === 'COMPLETED') {
|
if (status === 'COMPLETED') {
|
||||||
return 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
return 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
||||||
}
|
}
|
||||||
@@ -333,7 +353,8 @@ export function HomePage() {
|
|||||||
<h2 className="text-sm font-semibold capitalize">{formatDateLabel(date)}</h2>
|
<h2 className="text-sm font-semibold capitalize">{formatDateLabel(date)}</h2>
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{bookings.map((booking) => {
|
{bookings.map((booking) => {
|
||||||
const canManage = booking.status === 'CONFIRMED'
|
const effectiveStatus = getEffectiveStatus(booking)
|
||||||
|
const canManage = booking.status === 'CONFIRMED' && effectiveStatus === 'CONFIRMED'
|
||||||
const isMutating = updateStatusMutation.isPending
|
const isMutating = updateStatusMutation.isPending
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -352,9 +373,9 @@ export function HomePage() {
|
|||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`inline-flex rounded-full border px-2 py-1 text-xs font-medium ${statusClassName(booking.status)}`}
|
className={`inline-flex rounded-full border px-2 py-1 text-xs font-medium ${effectiveStatusClassName(effectiveStatus)}`}
|
||||||
>
|
>
|
||||||
{statusLabel(booking.status)}
|
{effectiveStatusLabel(effectiveStatus)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{canManage && (
|
{canManage && (
|
||||||
|
|||||||
@@ -73,13 +73,6 @@ export function RootLayout() {
|
|||||||
>
|
>
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
|
||||||
to="/about"
|
|
||||||
className="text-muted-foreground"
|
|
||||||
activeProps={{ className: 'text-foreground font-medium' }}
|
|
||||||
>
|
|
||||||
About
|
|
||||||
</Link>
|
|
||||||
{isAuthenticated && currentComplexSlug && (
|
{isAuthenticated && currentComplexSlug && (
|
||||||
<Link
|
<Link
|
||||||
to="/complex/$slug/edit"
|
to="/complex/$slug/edit"
|
||||||
|
|||||||
@@ -1,19 +1,91 @@
|
|||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
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 type { UserProfileResponse } from '@repo/api-contract'
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
import { apiClient } from '@/lib/api-client'
|
import { apiClient } from '@/lib/api-client'
|
||||||
import { useAuth } from '@/lib/auth'
|
import { useAuth } from '@/lib/auth'
|
||||||
|
|
||||||
|
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'],
|
||||||
|
})
|
||||||
|
|
||||||
|
type UpdateProfileForm = z.infer<typeof updateProfileSchema>
|
||||||
|
type UpdatePasswordForm = z.infer<typeof updatePasswordSchema>
|
||||||
|
|
||||||
export function ProfilePage() {
|
export function ProfilePage() {
|
||||||
const { user, displayName, initials, avatarUrl, isAuthenticated } = useAuth()
|
const { user, displayName, initials, avatarUrl, isAuthenticated, updateProfile, updatePassword } = useAuth()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const profileQuery = useQuery({
|
const profileQuery = useQuery({
|
||||||
queryKey: ['user-profile'],
|
queryKey: ['user-profile'],
|
||||||
enabled: isAuthenticated,
|
enabled: isAuthenticated,
|
||||||
queryFn: (): Promise<UserProfileResponse> => apiClient.user.getProfile(),
|
queryFn: (): Promise<UserProfileResponse> => apiClient.user.getProfile(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const updateProfileForm = useForm<UpdateProfileForm>({
|
||||||
|
resolver: zodResolver(updateProfileSchema),
|
||||||
|
defaultValues: {
|
||||||
|
fullName: displayName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatePasswordForm = useForm<UpdatePasswordForm>({
|
||||||
|
resolver: zodResolver(updatePasswordSchema),
|
||||||
|
defaultValues: {
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateProfileMutation = useMutation({
|
||||||
|
mutationFn: updateProfile,
|
||||||
|
onSuccess: () => {
|
||||||
|
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 })
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
updatePasswordForm.reset()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onUpdateProfile = (data: UpdateProfileForm) => {
|
||||||
|
updateProfileMutation.mutate(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUpdatePassword = (data: UpdatePasswordForm) => {
|
||||||
|
updatePasswordMutation.mutate(data)
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto flex min-h-[60vh] w-full max-w-6xl items-center justify-center px-6">
|
<main className="mx-auto flex min-h-[60vh] w-full max-w-6xl items-center justify-center px-6">
|
||||||
@@ -31,41 +103,143 @@ export function ProfilePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto flex min-h-[60vh] w-full max-w-6xl items-center justify-center px-6">
|
<main className="mx-auto flex min-h-[60vh] w-full max-w-6xl items-center justify-center px-6 py-8">
|
||||||
<section className="w-full max-w-3xl rounded-xl border bg-card p-6 text-card-foreground shadow-sm">
|
<div className="w-full max-w-3xl space-y-6">
|
||||||
<div className="flex items-center gap-3">
|
<section className="rounded-xl border bg-card p-6 text-card-foreground shadow-sm">
|
||||||
<Avatar size="lg">
|
<div className="flex items-center gap-3">
|
||||||
<AvatarImage
|
<Avatar size="lg">
|
||||||
src={profileQuery.data?.avatarUrl ?? avatarUrl ?? undefined}
|
<AvatarImage
|
||||||
alt={displayName}
|
src={profileQuery.data?.avatarUrl ?? avatarUrl ?? undefined}
|
||||||
/>
|
alt={displayName}
|
||||||
<AvatarFallback>{initials}</AvatarFallback>
|
/>
|
||||||
</Avatar>
|
<AvatarFallback>{initials}</AvatarFallback>
|
||||||
<div>
|
</Avatar>
|
||||||
<h1 className="text-2xl font-semibold">
|
<div>
|
||||||
{profileQuery.data?.fullName ?? displayName}
|
<h1 className="text-2xl font-semibold">
|
||||||
</h1>
|
{profileQuery.data?.fullName ?? displayName}
|
||||||
<p className="text-sm text-muted-foreground">
|
</h1>
|
||||||
{profileQuery.data?.email ?? user?.email}
|
|
||||||
</p>
|
|
||||||
{profileQuery.data?.role && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Rol: {profileQuery.data.role}
|
{profileQuery.data?.email ?? user?.email}
|
||||||
</p>
|
</p>
|
||||||
)}
|
{profileQuery.data?.role && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Rol: {profileQuery.data.role}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{profileQuery.isLoading && (
|
{profileQuery.isLoading && (
|
||||||
<p className="mt-4 text-sm text-muted-foreground">Cargando perfil...</p>
|
<p className="mt-4 text-sm text-muted-foreground">Cargando perfil...</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileQuery.isError && (
|
{profileQuery.isError && (
|
||||||
<p className="mt-4 text-sm text-destructive">
|
<p className="mt-4 text-sm text-destructive">
|
||||||
No se pudo cargar el perfil desde el backend.
|
No se pudo cargar el perfil desde el backend.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-xl border bg-card p-6 text-card-foreground shadow-sm">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Editar Perfil</h2>
|
||||||
|
<Form {...updateProfileForm}>
|
||||||
|
<form onSubmit={updateProfileForm.handleSubmit(onUpdateProfile)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={updateProfileForm.control}
|
||||||
|
name="fullName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nombre completo</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={updateProfileMutation.isPending}
|
||||||
|
>
|
||||||
|
{updateProfileMutation.isPending ? 'Guardando...' : 'Guardar cambios'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
{updateProfileMutation.isError && (
|
||||||
|
<p className="mt-4 text-sm text-destructive">
|
||||||
|
Error al actualizar el perfil: {updateProfileMutation.error?.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{updateProfileMutation.isSuccess && (
|
||||||
|
<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">
|
||||||
|
<FormField
|
||||||
|
control={updatePasswordForm.control}
|
||||||
|
name="currentPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Contraseña actual</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={updatePasswordForm.control}
|
||||||
|
name="newPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nueva contraseña</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={updatePasswordForm.control}
|
||||||
|
name="confirmPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Confirmar nueva contraseña</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={updatePasswordMutation.isPending}
|
||||||
|
>
|
||||||
|
{updatePasswordMutation.isPending ? 'Cambiando...' : 'Cambiar contraseña'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
{updatePasswordMutation.isError && (
|
||||||
|
<p className="mt-4 text-sm text-destructive">
|
||||||
|
Error al cambiar la contraseña: {updatePasswordMutation.error?.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{updatePasswordMutation.isSuccess && (
|
||||||
|
<p className="mt-4 text-sm text-green-600">
|
||||||
|
Contraseña cambiada exitosamente.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ type SignUpParams = {
|
|||||||
fullName: string
|
fullName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateProfileParams = {
|
||||||
|
fullName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePasswordParams = {
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
export type AuthContextValue = {
|
export type AuthContextValue = {
|
||||||
user: User | null
|
user: User | null
|
||||||
session: Session | null
|
session: Session | null
|
||||||
@@ -31,6 +39,8 @@ export type AuthContextValue = {
|
|||||||
avatarUrl: string | null
|
avatarUrl: string | null
|
||||||
signInWithPassword: (params: SignInParams) => Promise<void>
|
signInWithPassword: (params: SignInParams) => Promise<void>
|
||||||
signUp: (params: SignUpParams) => Promise<{ requiresEmailConfirmation: boolean }>
|
signUp: (params: SignUpParams) => Promise<{ requiresEmailConfirmation: boolean }>
|
||||||
|
updateProfile: (params: UpdateProfileParams) => Promise<void>
|
||||||
|
updatePassword: (params: UpdatePasswordParams) => Promise<void>
|
||||||
signOut: () => Promise<void>
|
signOut: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +153,27 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
|||||||
requiresEmailConfirmation: !data.session,
|
requiresEmailConfirmation: !data.session,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateProfile: async ({ fullName }) => {
|
||||||
|
const { error } = await supabase.auth.updateUser({
|
||||||
|
data: {
|
||||||
|
full_name: fullName,
|
||||||
|
name: fullName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updatePassword: async ({ password }) => {
|
||||||
|
const { error } = await supabase.auth.updateUser({
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
signOut: async () => {
|
signOut: async () => {
|
||||||
const { error } = await supabase.auth.signOut()
|
const { error } = await supabase.auth.signOut()
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user