add custom date time picker

This commit is contained in:
vergiLgood1 2025-03-10 23:54:28 +07:00
parent 5ee59cbf20
commit 280033b0e1
13 changed files with 1106 additions and 476 deletions

View File

@ -200,38 +200,11 @@ export function DataTable<TData, TValue>({
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
{/* {onActionClick && (
<TableCell>
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
onActionClick(row.original, "update");
}}
>
Update
</Button>
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
onActionClick(row.original, "delete");
}}
>
Delete
</Button>
</TableCell>
)} */}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length + 1}
className="h-24 text-center"
>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>

View File

@ -381,7 +381,7 @@ export function UserDetailSheet({
) : (
<>
<Ban className="h-4 w-4 mr-2" />
{user.banned_until ? "Unban user" : "Ban user"}
{user.banned_until ? "Unban user" : "Ban user"}
</>
)}
</Button>

View File

@ -1,80 +1,63 @@
import { FormDescription } from "@/app/_components/ui/form";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { format } from "date-fns";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/app/_components/ui/sheet";
import type React from "react"
import { useState } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import type * as z from "zod"
import { Loader2 } from "lucide-react"
import { UpdateUserParamsSchema, type User, UserSchema } from "@/src/models/users/users.model"
// UI Components
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/app/_components/ui/sheet"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/app/_components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/app/_components/ui/select";
import { Input } from "@/app/_components/ui/input";
import { Button } from "@/app/_components/ui/button";
import { Textarea } from "@/app/_components/ui/textarea";
import { Calendar } from "@/app/_components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/app/_components/ui/popover";
import { cn } from "@/lib/utils";
import { CalendarIcon, Loader2 } from "lucide-react";
import { Switch } from "@/app/_components/ui/switch";
import { User, UserSchema } from "@/src/models/users/users.model";
} from "@/app/_components/ui/form"
type UserProfileFormValues = z.infer<typeof UserSchema>;
import { Button } from "@/app/_components/ui/button"
import { FormSection } from "@/app/_components/form-section"
import { FormFieldWrapper } from "@/app/_components/form-wrapper"
import { useMutation } from "@tanstack/react-query"
import { updateUser } from "../action"
import { toast } from "sonner"
type UserProfileFormValues = z.infer<typeof UpdateUserParamsSchema>
interface UserProfileSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
userData?: User; // Replace with your user data type
onSave: (data: UserProfileFormValues) => Promise<void>;
open: boolean
onOpenChange: (open: boolean) => void
userData?: User
}
export function UserProfileSheet({
open,
onOpenChange,
userData,
onSave,
}: UserProfileSheetProps) {
const [isSaving, setIsSaving] = useState(false);
export function UserProfileSheet({ open, onOpenChange, userData }: UserProfileSheetProps) {
const [isSaving, setIsSaving] = useState(false)
// Initialize form with user data
const form = useForm<UserProfileFormValues>({
resolver: zodResolver(UserSchema),
resolver: zodResolver(UpdateUserParamsSchema),
defaultValues: {
email: userData?.email || "",
password_hash: userData?.password_hash || "",
role: (userData?.role as "user" | "staff" | "admin") || "user",
phone: userData?.phone || "",
role: userData?.role || "user",
invited_at: userData?.invited_at || undefined,
confirmed_at: userData?.confirmed_at || undefined,
recovery_sent_at: userData?.recovery_sent_at || undefined,
last_sign_in_at: userData?.last_sign_in_at || undefined,
created_at: userData?.created_at || undefined,
updated_at: userData?.updated_at || undefined,
is_anonymous: userData?.is_anonymous || false,
banned_until: userData?.banned_until ? String(userData.banned_until) : undefined,
profile: {
id: userData?.profile?.id || "",
user_id: userData?.profile?.user_id || "",
avatar: userData?.profile?.avatar || "",
username: userData?.profile?.username || "",
first_name: userData?.profile?.first_name || "",
last_name: userData?.profile?.last_name || "",
bio: userData?.profile?.bio || "",
birth_date: userData?.profile?.birth_date
? new Date(userData.profile.birth_date)
: null,
avatar: userData?.profile?.avatar || "",
address: userData?.profile?.address || {
street: "",
city: "",
@ -82,368 +65,246 @@ export function UserProfileSheet({
country: "",
postal_code: "",
},
birth_date: userData?.profile?.birth_date ? new Date(userData.profile.birth_date) : undefined,
},
},
});
})
const { mutate: updateUserMutation, isPending } = useMutation({
mutationKey: ["updateUser"],
mutationFn: (data: UserProfileFormValues) => {
if (!userData?.id) {
throw new Error("User ID is required");
}
return updateUser(userData.id, data);
},
onError: (error) => {
toast("Failed to update user");
},
onSuccess: () => {
toast("User updated");
},
})
async function onSubmit(data: UserProfileFormValues) {
try {
setIsSaving(true);
await onSave(data);
onOpenChange(false);
setIsSaving(true)
await updateUserMutation(data)
onOpenChange(false)
} catch (error) {
console.error("Error saving user profile:", error);
console.error("Error saving user profile:", error)
} finally {
setIsSaving(false);
setIsSaving(false)
}
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="w-full sm:max-w-xl overflow-y-auto">
<SheetHeader className="mb-6">
<SheetContent className="w-full sm:max-w-2xl overflow-y-auto">
<SheetHeader className="mb-6 border-b-2 pb-4">
<SheetTitle>Update User Profile</SheetTitle>
<SheetDescription>
Make changes to the user profile here. Click save when you&apos;re
done.
</SheetDescription>
<SheetDescription>Make changes to the user profile here. Click save when you&apos;re done.</SheetDescription>
</SheetHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* User Information Section */}
<div className="space-y-4">
<h3 className="text-lg font-medium">User Information</h3>
<FormField
control={form.control}
<FormSection
title="User Information"
description="Update the user information below. Fields marked with an asterisk (*) are required."
>
<FormFieldWrapper
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
{...field}
type="email"
placeholder="email@example.com"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
label="Email"
type="string"
control={form.control}
placeholder="email@example.com"
rows={4}
/>
<FormFieldWrapper
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>Phone</FormLabel>
<FormControl>
<Input
{...field}
type="tel"
placeholder="+1234567890"
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
label="Phone"
type="tel"
control={form.control}
placeholder="+1234567890"
/>
<FormFieldWrapper
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="user">User</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
label="Role"
type="string (select)"
control={form.control}
name="is_anonymous"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
Anonymous User
</FormLabel>
<FormDescription>
Make this user anonymous in the system
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
options={[
{ value: "user", label: "User" },
{ value: "admin", label: "Admin" },
{ value: "staff", label: "Staff" },
]}
/>
</div>
<FormFieldWrapper
name="is_anonymous"
label="Is Anonymous"
type="boolean"
control={form.control}
isBoolean={true}
booleanType="select"
/>
<FormFieldWrapper
name="password_hash"
label="Password Hash"
type="string"
control={form.control}
placeholder="Password Hash"
/>
<FormFieldWrapper
name="invited_at"
label="Invited At"
type="date"
control={form.control}
isDate={true}
/>
<FormFieldWrapper
name="confirmed_at"
label="Confirmed At"
type="date"
control={form.control}
isDate={true}
/>
<FormFieldWrapper
name="recovery_sent_at"
label="Recovery Sent At"
type="date"
control={form.control}
isDate={true}
/>
<FormFieldWrapper
name="last_sign_in_at"
label="Last Sign In At"
type="date"
control={form.control}
isDate={true}
/>
<FormFieldWrapper
name="created_at"
label="Created At"
type="date"
control={form.control}
isDate={true}
/>
<FormFieldWrapper
name="updated_at"
label="Updated At"
type="date"
control={form.control}
isDate={true}
/>
<FormFieldWrapper
name="banned_until"
label="Banned Until"
type="date"
control={form.control}
isDate={true}
/>
</FormSection>
{/* Profile Information Section */}
<div className="space-y-4">
<h3 className="text-lg font-medium">Profile Information</h3>
<FormField
control={form.control}
<FormSection title="Profile Information" description="Update the user profile information below.">
<FormFieldWrapper
name="profile.username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
{...field}
placeholder="username"
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="profile.first_name"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input
{...field}
placeholder="John"
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="profile.last_name"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input
{...field}
placeholder="Doe"
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
label="Username"
type="string"
control={form.control}
placeholder="username"
/>
<FormFieldWrapper
name="profile.first_name"
label="First Name"
type="string"
control={form.control}
placeholder="John"
/>
<FormFieldWrapper
name="profile.last_name"
label="Last Name"
type="string"
control={form.control}
placeholder="Doe"
/>
<FormFieldWrapper
name="profile.bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
{...field}
placeholder="Tell us about yourself"
className="resize-none"
rows={4}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
label="Bio"
type="string"
control={form.control}
placeholder="Tell us about yourself"
rows={4}
/>
<FormFieldWrapper
name="profile.birth_date"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Date of birth</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={cn(
"w-full pl-3 text-left font-normal",
!field.value && "text-muted-foreground"
)}
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={
field.value instanceof Date
? field.value
: undefined
}
onSelect={field.onChange}
disabled={(date) =>
date > new Date() || date < new Date("1900-01-01")
}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
label="Date of birth"
type="date"
control={form.control}
name="profile.avatar"
render={({ field }) => (
<FormItem>
<FormLabel>Avatar URL</FormLabel>
<FormControl>
<Input
{...field}
type="url"
placeholder="https://example.com/avatar.jpg"
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
isDate={true}
/>
</div>
<FormFieldWrapper
name="profile.avatar"
label="Avatar URL"
type="string (URL)"
control={form.control}
placeholder="https://example.com/avatar.jpg"
/>
</FormSection>
{/* Address Section */}
<div className="space-y-4">
<h3 className="text-lg font-medium">Address</h3>
<FormField
control={form.control}
<FormSection title="Address" description="Update the user address information below.">
<FormFieldWrapper
name="profile.address.street"
render={({ field }) => (
<FormItem>
<FormLabel>Street Address</FormLabel>
<FormControl>
<Input {...field} placeholder="123 Main St" />
</FormControl>
<FormMessage />
</FormItem>
)}
label="Street Address"
type="string"
control={form.control}
placeholder="123 Main St"
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="profile.address.city"
render={({ field }) => (
<FormItem>
<FormLabel>City</FormLabel>
<FormControl>
<Input {...field} placeholder="City" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="profile.address.state"
render={({ field }) => (
<FormItem>
<FormLabel>State</FormLabel>
<FormControl>
<Input {...field} placeholder="State" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="profile.address.country"
render={({ field }) => (
<FormItem>
<FormLabel>Country</FormLabel>
<FormControl>
<Input {...field} placeholder="Country" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="profile.address.postal_code"
render={({ field }) => (
<FormItem>
<FormLabel>Postal Code</FormLabel>
<FormControl>
<Input {...field} placeholder="Postal Code" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<FormFieldWrapper
name="profile.address.city"
label="City"
type="string"
control={form.control}
placeholder="City"
/>
<FormFieldWrapper
name="profile.address.state"
label="State"
type="string"
control={form.control}
placeholder="State"
/>
<FormFieldWrapper
name="profile.address.country"
label="Country"
type="string"
control={form.control}
placeholder="Country"
/>
<FormFieldWrapper
name="profile.address.postal_code"
label="Postal Code"
type="string"
control={form.control}
placeholder="Postal Code"
/>
</FormSection>
{/* Action Buttons */}
<div className="flex justify-end space-x-4 sticky bottom-0 bg-background py-4 border-t">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSaving}
>
<div className="flex justify-end space-x-4">
<Button type="button" variant="outline" size="xs" onClick={() => onOpenChange(false)} disabled={isPending}>
Cancel
</Button>
<Button type="submit" disabled={isSaving}>
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isSaving ? "Saving..." : "Save changes"}
<Button size="xs" type="submit" disabled={isPending}>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isPending ? "Saving..." : "Save"}
</Button>
</div>
</form>
</Form>
</SheetContent>
</Sheet>
);
)
}

View File

@ -1,26 +1,18 @@
"use client";
import { useState, useMemo, useEffect, JSX } from "react";
import { useState, useMemo, useEffect } from "react";
import {
PlusCircle,
Search,
Filter,
MoreHorizontal,
X,
ChevronDown,
UserPlus,
Mail,
SortAsc,
SortDesc,
Mail as MailIcon,
Phone,
Clock,
Calendar,
ShieldAlert,
ListFilter,
XCircle,
Trash2,
UserPen,
PenIcon as UserPen,
} from "lucide-react";
import { Button } from "@/app/_components/ui/button";
import { Input } from "@/app/_components/ui/input";
@ -35,15 +27,14 @@ import {
} from "@/app/_components/ui/dropdown-menu";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { fetchUsers } from "@/app/(protected)/(admin)/dashboard/user-management/action";
import { User } from "@/src/models/users/users.model";
import { toast } from "sonner";
import type { User } from "@/src/models/users/users.model";
import { DataTable } from "./data-table";
import { InviteUserDialog } from "./invite-user";
import { AddUserDialog } from "./add-user-dialog";
import { UserDetailSheet } from "./sheet";
import { Avatar } from "@radix-ui/react-avatar";
import Image from "next/image";
import { ColumnDef, HeaderContext } from "@tanstack/react-table";
import type { ColumnDef, HeaderContext } from "@tanstack/react-table";
import { UserProfileSheet } from "./update-user";
type UserFilterOptions = {
@ -58,7 +49,8 @@ type UserTableColumn = ColumnDef<User, User>;
export default function UserManagement() {
const [searchQuery, setSearchQuery] = useState("");
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [detailUser, setDetailUser] = useState<User | null>(null);
const [updateUser, setUpdateUser] = useState<User | null>(null);
const [isSheetOpen, setIsSheetOpen] = useState(false);
const [isUpdateOpen, setIsUpdateOpen] = useState(false);
const [isAddUserOpen, setIsAddUserOpen] = useState(false);
@ -86,17 +78,51 @@ export default function UserManagement() {
throwOnError: true,
});
// Handle opening the detail sheet
const handleUserClick = (user: User) => {
setSelectedUser(user);
setDetailUser(user);
setIsSheetOpen(true);
};
// Handle opening the update sheet
const handleUserUpdate = (user: User) => {
setSelectedUser(user);
setIsSheetOpen(false);
setUpdateUser(user);
setIsUpdateOpen(true);
};
// Close detail sheet when update sheet opens
useEffect(() => {
if (isUpdateOpen) {
setIsSheetOpen(false);
}
}, [isUpdateOpen]);
// Reset detail user when sheet closes
useEffect(() => {
if (!isSheetOpen) {
// Use a small delay to prevent flickering if another sheet is opening
const timer = setTimeout(() => {
if (!isSheetOpen && !isUpdateOpen) {
setDetailUser(null);
}
}, 300);
return () => clearTimeout(timer);
}
}, [isSheetOpen, isUpdateOpen]);
// Reset update user when update sheet closes
useEffect(() => {
if (!isUpdateOpen) {
// Use a small delay to prevent flickering if another sheet is opening
const timer = setTimeout(() => {
if (!isUpdateOpen) {
setUpdateUser(null);
}
}, 300);
return () => clearTimeout(timer);
}
}, [isUpdateOpen]);
const filteredUsers = useMemo(() => {
return users.filter((user) => {
// Global search
@ -253,7 +279,7 @@ export default function UserManagement() {
<Avatar className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
{row.original.profile?.avatar ? (
<Image
src={row.original.profile.avatar}
src={row.original.profile.avatar || "/placeholder.svg"}
alt="Avatar"
className="w-full h-full rounded-full"
width={32}
@ -519,35 +545,38 @@ export default function UserManagement() {
id: "actions",
header: "",
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleUserUpdate(row.original)}>
<UserPen className="h-4 w-4 mr-2 text-blue-500" />
Update
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
/* handle delete */
}}
>
<Trash2 className="h-4 w-4 mr-2 text-red-500" />
Delete
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
/* handle ban */
}}
>
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />
{row.original.banned_until != null ? "Unban" : "Ban"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div onClick={(e) => e.stopPropagation()}>
{/* Add this wrapper */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleUserUpdate(row.original)}>
<UserPen className="h-4 w-4 mr-2 text-blue-500" />
Update
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
/* handle delete */
}}
>
<Trash2 className="h-4 w-4 mr-2 text-red-500" />
Delete
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
/* handle ban */
}}
>
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />
{row.original.banned_until != null ? "Unban" : "Ban"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
},
];
@ -616,9 +645,9 @@ export default function UserManagement() {
loading={isLoading}
onRowClick={(user) => handleUserClick(user)}
/>
{selectedUser && (
{detailUser && (
<UserDetailSheet
user={selectedUser}
user={detailUser}
open={isSheetOpen}
onOpenChange={setIsSheetOpen}
onUserUpdate={() => {}}
@ -634,12 +663,11 @@ export default function UserManagement() {
onOpenChange={setIsInviteUserOpen}
onUserInvited={() => refetch()}
/>
{selectedUser && (
{updateUser && (
<UserProfileSheet
open={isUpdateOpen}
onOpenChange={setIsUpdateOpen}
userData={selectedUser}
onSave={async () => {}}
userData={updateUser}
/>
)}
</div>

View File

@ -162,9 +162,15 @@ export async function updateUser(
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
email: params.email,
phone: params.phone,
email_confirm: params.email_confirmed_at,
password: params.password_hash,
password_hash: params.password_hash,
ban_duration: params.ban_duration,
phone: params.phone,
phone_confirm: params.phone_confirmed_at,
role: params.role,
ban_duration: params.banned_until,
user_metadata: params.user_metadata,
app_metadata: params.app_metadata,
});
if (error) {
@ -190,16 +196,23 @@ export async function updateUser(
id: userId,
},
data: {
role: params.role,
role: params.role || user.role,
invited_at: params.invited_at || user.role,
confirmed_at: params.confirmed_at || user.role,
recovery_sent_at: params.recovery_sent_at || user.role,
last_sign_in_at: params.last_sign_in_at || user.role,
is_anonymous: params.is_anonymous || user.is_anonymous,
created_at: params.created_at || user.role,
updated_at: params.updated_at || user.role,
profile: {
update: {
avatar: params.profile.avatar || user.profile?.avatar,
username: params.profile.username || user.profile?.username,
first_name: params.profile.first_name || user.profile?.first_name,
last_name: params.profile.last_name || user.profile?.last_name,
bio: params.profile.bio || user.profile?.bio,
address: params.profile.address,
birth_date: params.profile.birth_date || user.profile?.birth_date,
avatar: params.profile?.avatar || user.profile?.avatar,
username: params.profile?.username || user.profile?.username,
first_name: params.profile?.first_name || user.profile?.first_name,
last_name: params.profile?.last_name || user.profile?.last_name,
bio: params.profile?.bio || user.profile?.bio,
address: params.profile?.address || user.profile?.address,
birth_date: params.profile?.birth_date || user.profile?.birth_date,
},
},
},

View File

@ -0,0 +1,417 @@
import type React from "react"
import { useEffect, useMemo, useState, useCallback, useRef } from "react"
import { ChevronLeft, ChevronRight, Clock } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { useVirtualizer } from "@tanstack/react-virtual"
import { cn } from "@/lib/utils"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
import { Button } from "@/app/_components/ui/button"
// Custom calendar component with enhanced year and month navigation
export function DateTimePicker({
selected,
onSelect,
disabled,
fromYear = 1900,
toYear = new Date().getFullYear() + 10,
showTimePicker = true,
className,
minuteStep = 1,
showSeconds = true,
}: {
selected?: Date
onSelect: (date?: Date) => void
disabled?: (date: Date) => boolean
fromYear?: number
toYear?: number
showTimePicker?: boolean
className?: string
minuteStep?: number
showSeconds?: boolean
}) {
// Initialize with selected date or current date
const [date, setDate] = useState<Date>(() => {
return selected ? new Date(selected) : new Date()
})
const [hours, setHours] = useState<string>(() => {
return selected ? String(selected.getHours()).padStart(2, "0") : String(new Date().getHours()).padStart(2, "0")
})
const [minutes, setMinutes] = useState<string>(() => {
if (selected) {
return String(selected.getMinutes()).padStart(2, "0")
}
// Round current minutes to nearest step
const currentMinutes = new Date().getMinutes()
const roundedMinutes = Math.round(currentMinutes / minuteStep) * minuteStep
return String(roundedMinutes % 60).padStart(2, "0")
})
const [seconds, setSeconds] = useState<string>(() => {
return selected ? String(selected.getSeconds()).padStart(2, "0") : String(0).padStart(2, "0")
})
// Track if we're in the middle of an update to prevent loops
const isUpdatingRef = useRef(false)
// Generate valid minute options based on minuteStep
const minuteOptions = useMemo(() => {
const options = []
for (let i = 0; i < 60; i += minuteStep) {
options.push(String(i).padStart(2, "0"))
}
return options
}, [minuteStep])
// Generate valid hour options
const hourOptions = useMemo(() => {
return Array.from({ length: 24 }, (_, i) => String(i).padStart(2, "0"))
}, [])
// Generate valid second options
const secondOptions = useMemo(() => {
return Array.from({ length: 60 }, (_, i) => String(i).padStart(2, "0"))
}, [])
// Update the parent component when date or time changes
useEffect(() => {
if (isUpdatingRef.current) return
if (date) {
const newDate = new Date(date)
const newHours = Number.parseInt(hours, 10)
const newMinutes = Number.parseInt(minutes, 10)
const newSeconds = Number.parseInt(seconds, 10)
newDate.setHours(newHours, newMinutes, newSeconds, 0)
// Only call onSelect if the date actually changed
if (!selected || Math.abs(newDate.getTime() - (selected?.getTime() || 0)) > 100) {
isUpdatingRef.current = true
onSelect(newDate)
// Use requestAnimationFrame instead of setTimeout for better performance
requestAnimationFrame(() => {
isUpdatingRef.current = false
})
}
} else {
onSelect(undefined)
}
}, [date, hours, minutes, seconds, onSelect, selected])
// Update internal state when selected prop changes
useEffect(() => {
if (isUpdatingRef.current) return
if (selected) {
setDate(new Date(selected))
setHours(String(selected.getHours()).padStart(2, "0"))
setMinutes(String(selected.getMinutes()).padStart(2, "0"))
setSeconds(String(selected.getSeconds()).padStart(2, "0"))
}
}, [selected])
// Generate years array from fromYear to toYear
const years = useMemo(() => {
const yearsArray = []
for (let i = toYear; i >= fromYear; i--) {
yearsArray.push(i)
}
return yearsArray
}, [fromYear, toYear])
const months = useMemo(
() => [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
],
[],
)
// Handle time input changes
const handleHoursChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
let value = e.target.value.replace(/\D/g, "")
if (value === "") {
setHours("00")
return
}
const numValue = Number.parseInt(value, 10)
if (numValue > 23) {
value = "23"
}
setHours(value.padStart(2, "0"))
}, [])
const handleMinutesChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
let value = e.target.value.replace(/\D/g, "")
if (value === "") {
setMinutes("00")
return
}
const numValue = Number.parseInt(value, 10)
if (numValue > 59) {
value = "59"
}
setMinutes(value.padStart(2, "0"))
}, [])
const handleSecondsChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
let value = e.target.value.replace(/\D/g, "")
if (value === "") {
setSeconds("00")
return
}
const numValue = Number.parseInt(value, 10)
if (numValue > 59) {
value = "59"
}
setSeconds(value.padStart(2, "0"))
}, [])
// Clear date selection
const handleClear = useCallback(() => {
const now = new Date()
setDate(now)
setHours("00")
setMinutes("00")
setSeconds("00")
onSelect(new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0))
}, [onSelect])
// Set date to now
const handleSetNow = useCallback(() => {
const now = new Date()
setDate(now)
setHours(String(now.getHours()).padStart(2, "0"))
const roundedMinutes = Math.round(now.getMinutes() / minuteStep) * minuteStep
setMinutes(String(roundedMinutes % 60).padStart(2, "0"))
setSeconds(String(now.getSeconds()).padStart(2, "0"))
onSelect(now)
}, [minuteStep, onSelect])
// Custom caption component with optimized year selection
const CustomCaption = useCallback(
({ displayMonth }: { displayMonth: Date }) => {
const month = displayMonth.getMonth()
const year = displayMonth.getFullYear()
const yearListRef = useRef<HTMLDivElement>(null)
const handleMonthChange = useCallback(
(newMonth: string) => {
const monthIndex = months.findIndex((m) => m === newMonth)
const newDate = new Date(date)
newDate.setMonth(monthIndex)
setDate(newDate)
},
[date],
)
const handleYearChange = useCallback(
(newYear: string) => {
const newDate = new Date(date)
newDate.setFullYear(Number.parseInt(newYear))
setDate(newDate)
},
[date],
)
// Create a virtualizer for the years list
const virtualizer = useVirtualizer({
count: years.length,
getScrollElement: () => yearListRef.current,
estimateSize: () => 36, // Approximate height of each year item
overscan: 5, // Reduced overscan for better performance
})
// Pre-scroll to current year when the select opens
const handleYearSelectOpen = useCallback(() => {
const yearIndex = years.findIndex((y) => y === year)
if (yearIndex !== -1 && yearListRef.current) {
// Use requestAnimationFrame for smoother scrolling
requestAnimationFrame(() => {
virtualizer.scrollToIndex(yearIndex, { align: "center" })
})
}
}, [virtualizer, years, year])
return (
<div className="flex items-center justify-center gap-2 py-2">
<Select value={months[month]} onValueChange={handleMonthChange}>
<SelectTrigger className="h-8 w-[110px] text-sm">
<SelectValue placeholder={months[month]} />
</SelectTrigger>
<SelectContent>
{months.map((monthName) => (
<SelectItem key={monthName} value={monthName} className="text-sm">
{monthName}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={year.toString()}
onValueChange={handleYearChange}
onOpenChange={(open) => open && handleYearSelectOpen()}
>
<SelectTrigger className="h-8 w-[90px] text-sm">
<SelectValue placeholder={year.toString()} />
</SelectTrigger>
<SelectContent
ref={yearListRef}
className="h-[200px] overflow-auto"
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<SelectItem
key={years[virtualItem.index]}
value={years[virtualItem.index].toString()}
className="text-sm absolute top-0 left-0 w-full"
style={{
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{years[virtualItem.index]}
</SelectItem>
))}
</div>
</SelectContent>
</Select>
</div>
)
},
[date, months, years],
)
return (
<div className={cn("space-y-4 p-2 border rounded-md shadow-sm", className)}>
<DayPicker
mode="single"
selected={date}
onSelect={(newDate) => newDate && setDate(newDate)}
disabled={disabled}
month={date}
onMonthChange={setDate}
components={{
IconLeft: () => <ChevronLeft className="h-4 w-4" />,
IconRight: () => <ChevronRight className="h-4 w-4" />,
Caption: CustomCaption,
}}
classNames={{
caption: "flex justify-center relative items-center",
nav: "space-x-1 flex items-center",
nav_button: cn(
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 hover:bg-muted rounded-md transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
"h-9 w-9 p-0 font-normal aria-selected:opacity-100 hover:bg-accent hover:text-accent-foreground rounded-md transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
),
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside: "text-muted-foreground opacity-50",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
}}
/>
{showTimePicker && (
<div className="border-t pt-3">
<div className="flex items-center justify-center space-x-1">
<Clock className="mr-2 h-4 w-4 text-muted-foreground" />
<div className="flex items-center">
<Select value={hours} onValueChange={setHours}>
<SelectTrigger className="h-8 w-16 text-center text-sm">
<SelectValue placeholder={hours} />
</SelectTrigger>
<SelectContent className="max-h-60">
{hourOptions.map((hour) => (
<SelectItem key={hour} value={hour}>
{hour}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="mx-1 text-sm">:</span>
<Select value={minutes} onValueChange={setMinutes}>
<SelectTrigger className="h-8 w-16 text-center text-sm">
<SelectValue placeholder={minutes} />
</SelectTrigger>
<SelectContent className="max-h-60">
{minuteOptions.map((minute) => (
<SelectItem key={minute} value={minute}>
{minute}
</SelectItem>
))}
</SelectContent>
</Select>
{showSeconds && (
<>
<span className="mx-1 text-sm">:</span>
<Select value={seconds} onValueChange={setSeconds}>
<SelectTrigger className="h-8 w-16 text-center text-sm">
<SelectValue placeholder={seconds} />
</SelectTrigger>
<SelectContent className="max-h-60">
{secondOptions.map((second) => (
<SelectItem key={second} value={second}>
{second}
</SelectItem>
))}
</SelectContent>
</Select>
</>
)}
</div>
</div>
</div>
)}
<div className="flex justify-between border-t pt-3">
<Button variant="outline" size="sm" onClick={handleClear}>
Clear
</Button>
<Button variant="outline" size="sm" onClick={handleSetNow}>
Now
</Button>
</div>
</div>
)
}

View File

@ -0,0 +1,18 @@
import { FormDescription } from "./ui/form";
// Section component to reduce repetition
export function FormSection({
title,
description,
children,
}: { title: string; description?: string; children: React.ReactNode }) {
return (
<div className="space-y-8 border-b-2 pb-8">
<div>
<h3 className="text-lg font-medium">{title}</h3>
{description && <FormDescription>{description}</FormDescription>}
</div>
{children}
</div>
)
}

View File

@ -0,0 +1,156 @@
"use client"
import { format } from "date-fns"
import { CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react"
import { cn } from "@/lib/utils"
// UI Components
import { FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription } from "@/app/_components/ui/form"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
import { Input } from "@/app/_components/ui/input"
import { Button } from "@/app/_components/ui/button"
import { Textarea } from "@/app/_components/ui/textarea"
import { Popover, PopoverContent, PopoverTrigger } from "@/app/_components/ui/popover"
import { Switch } from "@/app/_components/ui/switch"
import { Checkbox } from "@/app/_components/ui/checkbox"
import { DayPicker } from "react-day-picker"
import { DateTimePicker } from "@/app/_components/date-time-picker"
// Reusable form field component to reduce repetition
interface FormFieldProps {
name: string
label: string
type: string
control: any
placeholder?: string
options?: { value: string; label: string }[]
rows?: number
isDate?: boolean
isBoolean?: boolean
description?: string
booleanType?: "switch" | "checkbox" | "select"
fromYear?: number
toYear?: number
}
export function FormFieldWrapper({
name,
label,
type,
control,
placeholder,
options,
rows = 1,
isDate,
isBoolean,
description,
booleanType = "switch",
fromYear = 1900,
toYear = new Date().getFullYear(),
}: FormFieldProps) {
// Default boolean options for select
const booleanOptions = [
{ value: "false", label: "False" },
{ value: "true", label: "True" },
]
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem
className={
isBoolean && booleanType === "switch"
? "flex items-start justify-between rounded-lg border p-4"
: "flex justify-between items-start gap-4"
}
>
<div className={isBoolean && booleanType === "switch" ? "space-y-1" : "w-1/3"}>
<FormLabel className={isBoolean && booleanType === "switch" ? "text-base" : ""}>{label}</FormLabel>
<p className="text-xs text-muted-foreground">{type}</p>
{description && isBoolean && booleanType === "switch" && <FormDescription>{description}</FormDescription>}
</div>
<div className={isBoolean && booleanType === "switch" ? "" : "w-2/3"}>
<FormControl>
{isBoolean ? (
booleanType === "switch" ? (
<Switch checked={field.value} onCheckedChange={field.onChange} />
) : booleanType === "checkbox" ? (
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
) : (
<Select
onValueChange={(value) => field.onChange(value === "true")}
defaultValue={String(field.value)}
>
<SelectTrigger>
<SelectValue placeholder={placeholder || `Select ${label.toLowerCase()}`} />
</SelectTrigger>
<SelectContent>
{booleanOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
)
) : type.includes("select") && options ? (
<Select onValueChange={field.onChange} defaultValue={field.value}>
<SelectTrigger>
<SelectValue placeholder={placeholder || `Select ${label.toLowerCase()}`} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : isDate ? (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
>
{field.value ? format(field.value, "MM/dd/yyyy hh:mm:ss a") : <span>Pick a date and time</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<DateTimePicker
selected={field.value instanceof Date ? field.value : undefined}
onSelect={field.onChange}
disabled={(date) => date > new Date() || date < new Date(`${fromYear}-01-01`)}
fromYear={fromYear}
toYear={toYear}
showTimePicker
/>
</PopoverContent>
</Popover>
) : rows > 1 ? (
<Textarea
{...field}
className="resize-none"
rows={rows}
value={field.value ?? ""}
placeholder={placeholder}
/>
) : (
<Input
{...field}
type={type.includes("URL") ? "url" : type === "string" ? "text" : type}
placeholder={placeholder}
value={field.value ?? ""}
/>
)}
</FormControl>
{!(isBoolean && booleanType === "switch") && <FormMessage />}
</div>
</FormItem>
)}
/>
)
}

View File

@ -22,6 +22,7 @@ const buttonVariants = cva(
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
xs: "h-7 rounded-md px-2",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},

View File

@ -0,0 +1,118 @@
"use client"
import * as React from "react"
import { format, getMonth, getYear, setMonth, setYear } from "date-fns"
import { Calendar as CalendarIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/app/_components/ui/button"
import { Calendar } from "@/app/_components/ui/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/app/_components/ui/popover"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
interface DatePickerProps {
startYear?: number;
endYear?: number;
}
export function DatePicker({
startYear = getYear(new Date()) - 100,
endYear = getYear(new Date()) + 100,
}: DatePickerProps) {
const [date, setDate] = React.useState<Date>(new Date());
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
const years = Array.from(
{ length: endYear - startYear + 1 },
(_, i) => startYear + i
);
const handleMonthChange = (month: string) => {
const newDate = setMonth(date, months.indexOf(month));
setDate(newDate);
}
const handleYearChange = (year: string) => {
const newDate = setYear(date, parseInt(year));
setDate(newDate)
}
const handleSelect = (selectedData: Date | undefined) => {
if (selectedData) {
setDate(selectedData)
}
}
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-[250px] justify-start text-left font-normal",
!date && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<div className="flex justify-between p-2">
<Select
onValueChange={handleMonthChange}
value={months[getMonth(date)]}
>
<SelectTrigger className="w-[110px]">
<SelectValue placeholder="Month" />
</SelectTrigger>
<SelectContent>
{months.map(month => (
<SelectItem key={month} value={month}>{month}</SelectItem>
))}
</SelectContent>
</Select>
<Select
onValueChange={handleYearChange}
value={getYear(date).toString()}
>
<SelectTrigger className="w-[110px]">
<SelectValue placeholder="Year" />
</SelectTrigger>
<SelectContent>
{years.map(year => (
<SelectItem key={year} value={year.toString()}>{year}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Calendar
mode="single"
selected={date}
onSelect={handleSelect}
initialFocus
month={date}
onMonthChange={setDate}
/>
</PopoverContent>
</Popover>
)
}

View File

@ -27,6 +27,7 @@
"@tabler/icons-react": "^3.30.0",
"@tanstack/react-query": "^5.66.9",
"@tanstack/react-table": "^8.21.2",
"@tanstack/react-virtual": "^3.13.2",
"autoprefixer": "10.4.20",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@ -3130,6 +3131,23 @@
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.2.tgz",
"integrity": "sha512-LceSUgABBKF6HSsHK2ZqHzQ37IKV/jlaWbHm+NyTa3/WNb/JZVcThDuTainf+PixltOOcFCYXwxbLpOX9sCx+g==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.21.2",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz",
@ -3143,6 +3161,16 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.2",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.2.tgz",
"integrity": "sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",

View File

@ -32,6 +32,7 @@
"@tabler/icons-react": "^3.30.0",
"@tanstack/react-query": "^5.66.9",
"@tanstack/react-table": "^8.21.2",
"@tanstack/react-virtual": "^3.13.2",
"autoprefixer": "10.4.20",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",

View File

@ -94,19 +94,35 @@ export type CreateUserParams = z.infer<typeof CreateUserParamsSchema>;
export const UpdateUserParamsSchema = z.object({
email: z.string().email().optional(),
phone: z.string().optional(),
email_confirmed_at: z.boolean().optional(),
password_hash: z.string().optional(),
ban_duration: z.string().optional(),
role: z.enum(["user", "staff", "admin"]).optional(),
profile: z.object({
avatar: z.string().optional(),
username: z.string().optional(),
first_name: z.string().optional(),
last_name: z.string().optional(),
bio: z.string().optional(),
address: z.string().optional(),
birth_date: z.string().optional(),
}),
phone: z.string().optional(),
phone_confirmed_at: z.boolean().optional(),
invited_at: z.union([z.string(), z.date()]).optional(),
confirmed_at: z.union([z.string(), z.date()]).optional(),
recovery_sent_at: z.union([z.string(), z.date()]).optional(),
last_sign_in_at: z.union([z.string(), z.date()]).optional(),
created_at: z.union([z.string(), z.date()]).optional(),
updated_at: z.union([z.string(), z.date()]).optional(),
is_anonymous: z.boolean().optional(),
banned_until: z.string().optional(),
user_metadata: z.record(z.any()).optional(),
app_metadata: z.record(z.any()).optional(),
profile: z
.object({
id: z.string().optional(),
user_id: z.string(),
avatar: z.string().optional(),
username: z.string().optional(),
first_name: z.string().optional(),
last_name: z.string().optional(),
bio: z.string().optional(),
address: z.any().optional(),
birth_date: z.date().optional(),
})
.optional(),
});
export type UpdateUserParams = z.infer<typeof UpdateUserParamsSchema>;