menggunakan refect function untuk mendapatkan data terbaru
This commit is contained in:
parent
280033b0e1
commit
0dc1717704
|
@ -1,3 +1,4 @@
|
||||||
|
import { DateTimePicker2 } from "@/app/_components/ui/date-picker";
|
||||||
import { createClient } from "@/utils/supabase/server";
|
import { createClient } from "@/utils/supabase/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ export default async function DashboardPage() {
|
||||||
<pre className="text-xs font-mono p-3 rounded border overflow-auto">
|
<pre className="text-xs font-mono p-3 rounded border overflow-auto">
|
||||||
{JSON.stringify(user, null, 2)}
|
{JSON.stringify(user, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||||
|
|
|
@ -147,22 +147,6 @@ export function UserDetailSheet({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDeleteUser = () => {
|
|
||||||
deleteUserMutation.mutate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendPasswordRecovery = () => {
|
|
||||||
sendPasswordRecoveryMutation.mutate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendMagicLink = () => {
|
|
||||||
sendMagicLinkMutation.mutate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleBan = () => {
|
|
||||||
toggleBanMutation.mutate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopyItem = (item: string) => {
|
const handleCopyItem = (item: string) => {
|
||||||
navigator.clipboard.writeText(item);
|
navigator.clipboard.writeText(item);
|
||||||
toast.success("Copied to clipboard");
|
toast.success("Copied to clipboard");
|
||||||
|
@ -300,7 +284,7 @@ export function UserDetailSheet({
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleSendPasswordRecovery}
|
onClick={() => sendPasswordRecoveryMutation.mutate()}
|
||||||
disabled={isLoading.sendPasswordRecovery || !user.email}
|
disabled={isLoading.sendPasswordRecovery || !user.email}
|
||||||
>
|
>
|
||||||
{isLoading.sendPasswordRecovery ? (
|
{isLoading.sendPasswordRecovery ? (
|
||||||
|
@ -329,7 +313,7 @@ export function UserDetailSheet({
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleSendMagicLink}
|
onClick={() => sendMagicLinkMutation.mutate()}
|
||||||
disabled={isLoading.sendMagicLink || !user.email}
|
disabled={isLoading.sendMagicLink || !user.email}
|
||||||
>
|
>
|
||||||
{isLoading.sendMagicLink ? (
|
{isLoading.sendMagicLink ? (
|
||||||
|
@ -370,7 +354,7 @@ export function UserDetailSheet({
|
||||||
<Button
|
<Button
|
||||||
variant={user.banned_until ? "outline" : "outline"}
|
variant={user.banned_until ? "outline" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleToggleBan}
|
onClick={() => toggleBanMutation.mutate()}
|
||||||
disabled={isLoading.toggleBan}
|
disabled={isLoading.toggleBan}
|
||||||
>
|
>
|
||||||
{isLoading.toggleBan ? (
|
{isLoading.toggleBan ? (
|
||||||
|
@ -428,7 +412,7 @@ export function UserDetailSheet({
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={handleDeleteUser}
|
onClick={() => deleteUserMutation.mutate()}
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
>
|
>
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { FormFieldWrapper } from "@/app/_components/form-wrapper"
|
||||||
import { useMutation } from "@tanstack/react-query"
|
import { useMutation } from "@tanstack/react-query"
|
||||||
import { updateUser } from "../action"
|
import { updateUser } from "../action"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
import { DateTimePicker2 } from "@/app/_components/ui/date-picker"
|
||||||
|
|
||||||
|
|
||||||
type UserProfileFormValues = z.infer<typeof UpdateUserParamsSchema>
|
type UserProfileFormValues = z.infer<typeof UpdateUserParamsSchema>
|
||||||
|
@ -49,7 +50,6 @@ export function UserProfileSheet({ open, onOpenChange, userData }: UserProfileSh
|
||||||
created_at: userData?.created_at || undefined,
|
created_at: userData?.created_at || undefined,
|
||||||
updated_at: userData?.updated_at || undefined,
|
updated_at: userData?.updated_at || undefined,
|
||||||
is_anonymous: userData?.is_anonymous || false,
|
is_anonymous: userData?.is_anonymous || false,
|
||||||
banned_until: userData?.banned_until ? String(userData.banned_until) : undefined,
|
|
||||||
profile: {
|
profile: {
|
||||||
id: userData?.profile?.id || "",
|
id: userData?.profile?.id || "",
|
||||||
user_id: userData?.profile?.user_id || "",
|
user_id: userData?.profile?.user_id || "",
|
||||||
|
@ -113,6 +113,7 @@ export function UserProfileSheet({ open, onOpenChange, userData }: UserProfileSh
|
||||||
title="User Information"
|
title="User Information"
|
||||||
description="Update the user information below. Fields marked with an asterisk (*) are required."
|
description="Update the user information below. Fields marked with an asterisk (*) are required."
|
||||||
>
|
>
|
||||||
|
|
||||||
<FormFieldWrapper
|
<FormFieldWrapper
|
||||||
name="email"
|
name="email"
|
||||||
label="Email"
|
label="Email"
|
||||||
|
@ -196,13 +197,7 @@ export function UserProfileSheet({ open, onOpenChange, userData }: UserProfileSh
|
||||||
control={form.control}
|
control={form.control}
|
||||||
isDate={true}
|
isDate={true}
|
||||||
/>
|
/>
|
||||||
<FormFieldWrapper
|
|
||||||
name="banned_until"
|
|
||||||
label="Banned Until"
|
|
||||||
type="date"
|
|
||||||
control={form.control}
|
|
||||||
isDate={true}
|
|
||||||
/>
|
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
{/* Profile Information Section */}
|
{/* Profile Information Section */}
|
||||||
|
|
|
@ -650,7 +650,7 @@ export default function UserManagement() {
|
||||||
user={detailUser}
|
user={detailUser}
|
||||||
open={isSheetOpen}
|
open={isSheetOpen}
|
||||||
onOpenChange={setIsSheetOpen}
|
onOpenChange={setIsSheetOpen}
|
||||||
onUserUpdate={() => {}}
|
onUserUpdate={() => refetch()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AddUserDialog
|
<AddUserDialog
|
||||||
|
@ -668,6 +668,7 @@ export default function UserManagement() {
|
||||||
open={isUpdateOpen}
|
open={isUpdateOpen}
|
||||||
onOpenChange={setIsUpdateOpen}
|
onOpenChange={setIsUpdateOpen}
|
||||||
userData={updateUser}
|
userData={updateUser}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -163,12 +163,11 @@ export async function updateUser(
|
||||||
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
||||||
email: params.email,
|
email: params.email,
|
||||||
email_confirm: params.email_confirmed_at,
|
email_confirm: params.email_confirmed_at,
|
||||||
password: params.password_hash,
|
password: params.password_hash ?? undefined,
|
||||||
password_hash: params.password_hash,
|
password_hash: params.password_hash ?? undefined,
|
||||||
phone: params.phone,
|
phone: params.phone,
|
||||||
phone_confirm: params.phone_confirmed_at,
|
phone_confirm: params.phone_confirmed_at,
|
||||||
role: params.role,
|
role: params.role,
|
||||||
ban_duration: params.banned_until,
|
|
||||||
user_metadata: params.user_metadata,
|
user_metadata: params.user_metadata,
|
||||||
app_metadata: params.app_metadata,
|
app_metadata: params.app_metadata,
|
||||||
});
|
});
|
||||||
|
@ -317,6 +316,28 @@ export async function unbanUser(userId: string): Promise<UserResponse> {
|
||||||
throw new Error(error.message);
|
throw new Error(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = await db.users.findUnique({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
banned_until: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// const updateUser = await db.users.update({
|
||||||
|
// where: {
|
||||||
|
// id: userId,
|
||||||
|
// },
|
||||||
|
// data: {
|
||||||
|
// banned_until: null,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
user: data.user,
|
user: data.user,
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
|
|
||||||
import { useEffect, useMemo, useState, useCallback, useRef } from "react"
|
import { useEffect, useMemo, useState, useCallback, useRef } from "react"
|
||||||
|
@ -9,6 +7,7 @@ import { useVirtualizer } from "@tanstack/react-virtual"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
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 { Button } from "@/app/_components/ui/button"
|
||||||
|
|
||||||
// Custom calendar component with enhanced year and month navigation
|
// Custom calendar component with enhanced year and month navigation
|
||||||
|
@ -23,15 +22,15 @@ export function DateTimePicker({
|
||||||
minuteStep = 1,
|
minuteStep = 1,
|
||||||
showSeconds = true,
|
showSeconds = true,
|
||||||
}: {
|
}: {
|
||||||
selected?: Date
|
selected?: Date
|
||||||
onSelect: (date?: Date) => void
|
onSelect: (date?: Date) => void
|
||||||
disabled?: (date: Date) => boolean
|
disabled?: (date: Date) => boolean
|
||||||
fromYear?: number
|
fromYear?: number
|
||||||
toYear?: number
|
toYear?: number
|
||||||
showTimePicker?: boolean
|
showTimePicker?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
minuteStep?: number
|
minuteStep?: number
|
||||||
showSeconds?: boolean
|
showSeconds?: boolean
|
||||||
}) {
|
}) {
|
||||||
// Initialize with selected date or current date
|
// Initialize with selected date or current date
|
||||||
const [date, setDate] = useState<Date>(() => {
|
const [date, setDate] = useState<Date>(() => {
|
||||||
|
@ -84,20 +83,18 @@ export function DateTimePicker({
|
||||||
|
|
||||||
if (date) {
|
if (date) {
|
||||||
const newDate = new Date(date)
|
const newDate = new Date(date)
|
||||||
const newHours = Number.parseInt(hours, 10)
|
newDate.setHours(Number.parseInt(hours, 10))
|
||||||
const newMinutes = Number.parseInt(minutes, 10)
|
newDate.setMinutes(Number.parseInt(minutes, 10))
|
||||||
const newSeconds = Number.parseInt(seconds, 10)
|
newDate.setSeconds(Number.parseInt(seconds, 10))
|
||||||
|
newDate.setMilliseconds(0)
|
||||||
newDate.setHours(newHours, newMinutes, newSeconds, 0)
|
|
||||||
|
|
||||||
// Only call onSelect if the date actually changed
|
// Only call onSelect if the date actually changed
|
||||||
if (!selected || Math.abs(newDate.getTime() - (selected?.getTime() || 0)) > 100) {
|
if (!selected || Math.abs(newDate.getTime() - (selected?.getTime() || 0)) > 100) {
|
||||||
isUpdatingRef.current = true
|
isUpdatingRef.current = true
|
||||||
onSelect(newDate)
|
onSelect(newDate)
|
||||||
// Use requestAnimationFrame instead of setTimeout for better performance
|
setTimeout(() => {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
isUpdatingRef.current = false
|
isUpdatingRef.current = false
|
||||||
})
|
}, 0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onSelect(undefined)
|
onSelect(undefined)
|
||||||
|
@ -117,13 +114,7 @@ export function DateTimePicker({
|
||||||
}, [selected])
|
}, [selected])
|
||||||
|
|
||||||
// Generate years array from fromYear to toYear
|
// Generate years array from fromYear to toYear
|
||||||
const years = useMemo(() => {
|
const years = useMemo(() => Array.from({ length: toYear - fromYear + 1 }, (_, i) => toYear - i), [fromYear, toYear])
|
||||||
const yearsArray = []
|
|
||||||
for (let i = toYear; i >= fromYear; i--) {
|
|
||||||
yearsArray.push(i)
|
|
||||||
}
|
|
||||||
return yearsArray
|
|
||||||
}, [fromYear, toYear])
|
|
||||||
|
|
||||||
const months = useMemo(
|
const months = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
@ -212,7 +203,6 @@ export function DateTimePicker({
|
||||||
({ displayMonth }: { displayMonth: Date }) => {
|
({ displayMonth }: { displayMonth: Date }) => {
|
||||||
const month = displayMonth.getMonth()
|
const month = displayMonth.getMonth()
|
||||||
const year = displayMonth.getFullYear()
|
const year = displayMonth.getFullYear()
|
||||||
const yearListRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const handleMonthChange = useCallback(
|
const handleMonthChange = useCallback(
|
||||||
(newMonth: string) => {
|
(newMonth: string) => {
|
||||||
|
@ -221,7 +211,7 @@ export function DateTimePicker({
|
||||||
newDate.setMonth(monthIndex)
|
newDate.setMonth(monthIndex)
|
||||||
setDate(newDate)
|
setDate(newDate)
|
||||||
},
|
},
|
||||||
[date],
|
[date, months],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleYearChange = useCallback(
|
const handleYearChange = useCallback(
|
||||||
|
@ -233,25 +223,6 @@ export function DateTimePicker({
|
||||||
[date],
|
[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 (
|
return (
|
||||||
<div className="flex items-center justify-center gap-2 py-2">
|
<div className="flex items-center justify-center gap-2 py-2">
|
||||||
<Select value={months[month]} onValueChange={handleMonthChange}>
|
<Select value={months[month]} onValueChange={handleMonthChange}>
|
||||||
|
@ -267,40 +238,16 @@ export function DateTimePicker({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select
|
<Select value={year.toString()} onValueChange={handleYearChange}>
|
||||||
value={year.toString()}
|
|
||||||
onValueChange={handleYearChange}
|
|
||||||
onOpenChange={(open) => open && handleYearSelectOpen()}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 w-[90px] text-sm">
|
<SelectTrigger className="h-8 w-[90px] text-sm">
|
||||||
<SelectValue placeholder={year.toString()} />
|
<SelectValue placeholder={year.toString()} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent
|
<SelectContent className="h-[200px] overflow-auto">
|
||||||
ref={yearListRef}
|
{years.map((yearValue) => (
|
||||||
className="h-[200px] overflow-auto"
|
<SelectItem key={yearValue} value={yearValue.toString()} className="text-sm">
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
{yearValue}
|
||||||
>
|
</SelectItem>
|
||||||
<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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -413,5 +360,4 @@ export function DateTimePicker({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { Switch } from "@/app/_components/ui/switch"
|
||||||
import { Checkbox } from "@/app/_components/ui/checkbox"
|
import { Checkbox } from "@/app/_components/ui/checkbox"
|
||||||
import { DayPicker } from "react-day-picker"
|
import { DayPicker } from "react-day-picker"
|
||||||
import { DateTimePicker } from "@/app/_components/date-time-picker"
|
import { DateTimePicker } from "@/app/_components/date-time-picker"
|
||||||
|
import { DateTimePicker2 } from "./ui/date-picker"
|
||||||
|
|
||||||
// Reusable form field component to reduce repetition
|
// Reusable form field component to reduce repetition
|
||||||
interface FormFieldProps {
|
interface FormFieldProps {
|
||||||
|
@ -127,6 +128,7 @@ export function FormFieldWrapper({
|
||||||
toYear={toYear}
|
toYear={toYear}
|
||||||
showTimePicker
|
showTimePicker
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
) : rows > 1 ? (
|
) : rows > 1 ? (
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { format, getMonth, getYear, setMonth, setYear } from "date-fns"
|
import { format, getMonth, getYear, setMonth, setYear, setHours, setMinutes, setSeconds } from "date-fns"
|
||||||
import { Calendar as CalendarIcon } from "lucide-react"
|
import { Calendar as CalendarIcon, Clock } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/app/_components/ui/button"
|
import { Button } from "@/app/_components/ui/button"
|
||||||
|
@ -13,37 +13,35 @@ import {
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/app/_components/ui/popover"
|
} from "@/app/_components/ui/popover"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
|
||||||
|
|
||||||
interface DatePickerProps {
|
interface DateTimePickerProps {
|
||||||
startYear?: number;
|
startYear?: number;
|
||||||
endYear?: number;
|
endYear?: number;
|
||||||
}
|
}
|
||||||
export function DatePicker({
|
|
||||||
|
export function DateTimePicker2({
|
||||||
startYear = getYear(new Date()) - 100,
|
startYear = getYear(new Date()) - 100,
|
||||||
endYear = getYear(new Date()) + 100,
|
endYear = getYear(new Date()) + 100,
|
||||||
}: DatePickerProps) {
|
}: DateTimePickerProps) {
|
||||||
|
|
||||||
const [date, setDate] = React.useState<Date>(new Date());
|
const [date, setDate] = React.useState<Date>(new Date());
|
||||||
|
|
||||||
const months = [
|
const months = [
|
||||||
'January',
|
'January', 'February', 'March', 'April', 'May', 'June',
|
||||||
'February',
|
'July', 'August', 'September', 'October', 'November', 'December',
|
||||||
'March',
|
|
||||||
'April',
|
|
||||||
'May',
|
|
||||||
'June',
|
|
||||||
'July',
|
|
||||||
'August',
|
|
||||||
'September',
|
|
||||||
'October',
|
|
||||||
'November',
|
|
||||||
'December',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const years = Array.from(
|
const years = Array.from(
|
||||||
{ length: endYear - startYear + 1 },
|
{ length: endYear - startYear + 1 },
|
||||||
(_, i) => startYear + i
|
(_, i) => startYear + i
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Generate hours (0-23)
|
||||||
|
const hours = Array.from({ length: 24 }, (_, i) => i);
|
||||||
|
|
||||||
|
// Generate minutes and seconds (0-59)
|
||||||
|
const minutesSeconds = Array.from({ length: 60 }, (_, i) => i);
|
||||||
|
|
||||||
const handleMonthChange = (month: string) => {
|
const handleMonthChange = (month: string) => {
|
||||||
const newDate = setMonth(date, months.indexOf(month));
|
const newDate = setMonth(date, months.indexOf(month));
|
||||||
setDate(newDate);
|
setDate(newDate);
|
||||||
|
@ -51,67 +49,170 @@ export function DatePicker({
|
||||||
|
|
||||||
const handleYearChange = (year: string) => {
|
const handleYearChange = (year: string) => {
|
||||||
const newDate = setYear(date, parseInt(year));
|
const newDate = setYear(date, parseInt(year));
|
||||||
setDate(newDate)
|
setDate(newDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = (selectedData: Date | undefined) => {
|
const handleHourChange = (hour: string) => {
|
||||||
if (selectedData) {
|
const newDate = setHours(date, parseInt(hour));
|
||||||
setDate(selectedData)
|
setDate(newDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMinuteChange = (minute: string) => {
|
||||||
|
const newDate = setMinutes(date, parseInt(minute));
|
||||||
|
setDate(newDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSecondChange = (second: string) => {
|
||||||
|
const newDate = setSeconds(date, parseInt(second));
|
||||||
|
setDate(newDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelect = (selectedDate: Date | undefined) => {
|
||||||
|
if (selectedDate) {
|
||||||
|
// Preserve time when changing date
|
||||||
|
const newDate = new Date(selectedDate);
|
||||||
|
newDate.setHours(date.getHours(), date.getMinutes(), date.getSeconds());
|
||||||
|
setDate(newDate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format number to always have 2 digits (e.g., 1 -> 01)
|
||||||
|
const formatTwoDigits = (num: number) => num.toString().padStart(2, '0');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant={"outline"}
|
variant={"outline"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-[250px] justify-start text-left font-normal",
|
"w-[280px] justify-start text-left font-normal",
|
||||||
!date && "text-muted-foreground"
|
!date && "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
{date ? format(date, "PPP") : <span>Pick a date</span>}
|
{date ? format(date, "PPP") + " " + format(date, "HH:mm:ss") : <span>Pick a date and time</span>}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0">
|
<PopoverContent className="w-auto p-0">
|
||||||
<div className="flex justify-between p-2">
|
<Tabs defaultValue="date">
|
||||||
<Select
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
onValueChange={handleMonthChange}
|
<TabsTrigger value="date">
|
||||||
value={months[getMonth(date)]}
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
>
|
Date
|
||||||
<SelectTrigger className="w-[110px]">
|
</TabsTrigger>
|
||||||
<SelectValue placeholder="Month" />
|
<TabsTrigger value="time">
|
||||||
</SelectTrigger>
|
<Clock className="mr-2 h-4 w-4" />
|
||||||
<SelectContent>
|
Time
|
||||||
{months.map(month => (
|
</TabsTrigger>
|
||||||
<SelectItem key={month} value={month}>{month}</SelectItem>
|
</TabsList>
|
||||||
))}
|
|
||||||
</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
|
<TabsContent value="date" className="p-0">
|
||||||
mode="single"
|
<div className="flex justify-between p-2">
|
||||||
selected={date}
|
<Select
|
||||||
onSelect={handleSelect}
|
onValueChange={handleMonthChange}
|
||||||
initialFocus
|
value={months[getMonth(date)]}
|
||||||
month={date}
|
>
|
||||||
onMonthChange={setDate}
|
<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}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="time" className="p-4">
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="text-center text-lg font-medium">
|
||||||
|
{format(date, "HH:mm:ss")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1 block">Hour</label>
|
||||||
|
<Select
|
||||||
|
onValueChange={handleHourChange}
|
||||||
|
value={date.getHours().toString()}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Hour" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{hours.map(hour => (
|
||||||
|
<SelectItem key={hour} value={hour.toString()}>
|
||||||
|
{formatTwoDigits(hour)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1 block">Minute</label>
|
||||||
|
<Select
|
||||||
|
onValueChange={handleMinuteChange}
|
||||||
|
value={date.getMinutes().toString()}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Minute" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{minutesSeconds.map(minute => (
|
||||||
|
<SelectItem key={minute} value={minute.toString()}>
|
||||||
|
{formatTwoDigits(minute)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1 block">Second</label>
|
||||||
|
<Select
|
||||||
|
onValueChange={handleSecondChange}
|
||||||
|
value={date.getSeconds().toString()}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Second" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{minutesSeconds.map(second => (
|
||||||
|
<SelectItem key={second} value={second.toString()}>
|
||||||
|
{formatTwoDigits(second)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,7 @@ const ReactQueryProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
// Pengaturan caching global
|
// Pengaturan caching global
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
gcTime: 10 * 60 * 1000,
|
gcTime: 10 * 60 * 1000,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: true,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
retryDelay: (attemptIndex) =>
|
retryDelay: (attemptIndex) =>
|
||||||
Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff
|
Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff
|
||||||
|
|
|
@ -106,7 +106,6 @@ export const UpdateUserParamsSchema = z.object({
|
||||||
created_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(),
|
updated_at: z.union([z.string(), z.date()]).optional(),
|
||||||
is_anonymous: z.boolean().optional(),
|
is_anonymous: z.boolean().optional(),
|
||||||
banned_until: z.string().optional(),
|
|
||||||
user_metadata: z.record(z.any()).optional(),
|
user_metadata: z.record(z.any()).optional(),
|
||||||
app_metadata: z.record(z.any()).optional(),
|
app_metadata: z.record(z.any()).optional(),
|
||||||
profile: z
|
profile: z
|
||||||
|
|
Loading…
Reference in New Issue