menggunakan refect function untuk mendapatkan data terbaru

This commit is contained in:
vergiLgood1 2025-03-11 01:05:03 +07:00
parent 280033b0e1
commit 0dc1717704
10 changed files with 225 additions and 174 deletions

View File

@ -1,3 +1,4 @@
import { DateTimePicker2 } from "@/app/_components/ui/date-picker";
import { createClient } from "@/utils/supabase/server";
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">
{JSON.stringify(user, null, 2)}
</pre>
</div>
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />

View File

@ -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) => {
navigator.clipboard.writeText(item);
toast.success("Copied to clipboard");
@ -300,7 +284,7 @@ export function UserDetailSheet({
<Button
variant="outline"
size="sm"
onClick={handleSendPasswordRecovery}
onClick={() => sendPasswordRecoveryMutation.mutate()}
disabled={isLoading.sendPasswordRecovery || !user.email}
>
{isLoading.sendPasswordRecovery ? (
@ -329,7 +313,7 @@ export function UserDetailSheet({
<Button
variant="outline"
size="sm"
onClick={handleSendMagicLink}
onClick={() => sendMagicLinkMutation.mutate()}
disabled={isLoading.sendMagicLink || !user.email}
>
{isLoading.sendMagicLink ? (
@ -370,7 +354,7 @@ export function UserDetailSheet({
<Button
variant={user.banned_until ? "outline" : "outline"}
size="sm"
onClick={handleToggleBan}
onClick={() => toggleBanMutation.mutate()}
disabled={isLoading.toggleBan}
>
{isLoading.toggleBan ? (
@ -428,7 +412,7 @@ export function UserDetailSheet({
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteUser}
onClick={() => deleteUserMutation.mutate()}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={isDeleting}
>

View File

@ -22,6 +22,7 @@ import { FormFieldWrapper } from "@/app/_components/form-wrapper"
import { useMutation } from "@tanstack/react-query"
import { updateUser } from "../action"
import { toast } from "sonner"
import { DateTimePicker2 } from "@/app/_components/ui/date-picker"
type UserProfileFormValues = z.infer<typeof UpdateUserParamsSchema>
@ -49,7 +50,6 @@ export function UserProfileSheet({ open, onOpenChange, userData }: UserProfileSh
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 || "",
@ -113,6 +113,7 @@ export function UserProfileSheet({ open, onOpenChange, userData }: UserProfileSh
title="User Information"
description="Update the user information below. Fields marked with an asterisk (*) are required."
>
<FormFieldWrapper
name="email"
label="Email"
@ -196,13 +197,7 @@ export function UserProfileSheet({ open, onOpenChange, userData }: UserProfileSh
control={form.control}
isDate={true}
/>
<FormFieldWrapper
name="banned_until"
label="Banned Until"
type="date"
control={form.control}
isDate={true}
/>
</FormSection>
{/* Profile Information Section */}

View File

@ -650,7 +650,7 @@ export default function UserManagement() {
user={detailUser}
open={isSheetOpen}
onOpenChange={setIsSheetOpen}
onUserUpdate={() => {}}
onUserUpdate={() => refetch()}
/>
)}
<AddUserDialog
@ -668,6 +668,7 @@ export default function UserManagement() {
open={isUpdateOpen}
onOpenChange={setIsUpdateOpen}
userData={updateUser}
/>
)}
</div>

View File

@ -163,12 +163,11 @@ export async function updateUser(
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
email: params.email,
email_confirm: params.email_confirmed_at,
password: params.password_hash,
password_hash: params.password_hash,
password: params.password_hash ?? undefined,
password_hash: params.password_hash ?? undefined,
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,
});
@ -317,6 +316,28 @@ export async function unbanUser(userId: string): Promise<UserResponse> {
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 {
data: {
user: data.user,

View File

@ -1,5 +1,3 @@
import type React 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
import { Input } from "@/app/_components/ui/input"
import { Button } from "@/app/_components/ui/button"
// Custom calendar component with enhanced year and month navigation
@ -23,15 +22,15 @@ export function DateTimePicker({
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
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>(() => {
@ -84,20 +83,18 @@ export function DateTimePicker({
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)
newDate.setHours(Number.parseInt(hours, 10))
newDate.setMinutes(Number.parseInt(minutes, 10))
newDate.setSeconds(Number.parseInt(seconds, 10))
newDate.setMilliseconds(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(() => {
setTimeout(() => {
isUpdatingRef.current = false
})
}, 0)
}
} else {
onSelect(undefined)
@ -117,13 +114,7 @@ export function DateTimePicker({
}, [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 years = useMemo(() => Array.from({ length: toYear - fromYear + 1 }, (_, i) => toYear - i), [fromYear, toYear])
const months = useMemo(
() => [
@ -212,7 +203,6 @@ export function DateTimePicker({
({ displayMonth }: { displayMonth: Date }) => {
const month = displayMonth.getMonth()
const year = displayMonth.getFullYear()
const yearListRef = useRef<HTMLDivElement>(null)
const handleMonthChange = useCallback(
(newMonth: string) => {
@ -221,7 +211,7 @@ export function DateTimePicker({
newDate.setMonth(monthIndex)
setDate(newDate)
},
[date],
[date, months],
)
const handleYearChange = useCallback(
@ -233,25 +223,6 @@ export function DateTimePicker({
[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}>
@ -267,40 +238,16 @@ export function DateTimePicker({
</SelectContent>
</Select>
<Select
value={year.toString()}
onValueChange={handleYearChange}
onOpenChange={(open) => open && handleYearSelectOpen()}
>
<Select value={year.toString()} onValueChange={handleYearChange}>
<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 className="h-[200px] overflow-auto">
{years.map((yearValue) => (
<SelectItem key={yearValue} value={yearValue.toString()} className="text-sm">
{yearValue}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
@ -413,5 +360,4 @@ export function DateTimePicker({
</div>
</div>
)
}
}

View File

@ -14,6 +14,7 @@ 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"
import { DateTimePicker2 } from "./ui/date-picker"
// Reusable form field component to reduce repetition
interface FormFieldProps {
@ -127,6 +128,7 @@ export function FormFieldWrapper({
toYear={toYear}
showTimePicker
/>
</PopoverContent>
</Popover>
) : rows > 1 ? (

View File

@ -1,8 +1,8 @@
"use client"
import * as React from "react"
import { format, getMonth, getYear, setMonth, setYear } from "date-fns"
import { Calendar as CalendarIcon } from "lucide-react"
import { format, getMonth, getYear, setMonth, setYear, setHours, setMinutes, setSeconds } from "date-fns"
import { Calendar as CalendarIcon, Clock } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/app/_components/ui/button"
@ -13,37 +13,35 @@ import {
PopoverTrigger,
} from "@/app/_components/ui/popover"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
interface DatePickerProps {
interface DateTimePickerProps {
startYear?: number;
endYear?: number;
}
export function DatePicker({
export function DateTimePicker2({
startYear = getYear(new Date()) - 100,
endYear = getYear(new Date()) + 100,
}: DatePickerProps) {
}: DateTimePickerProps) {
const [date, setDate] = React.useState<Date>(new Date());
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
];
const years = Array.from(
{ length: endYear - startYear + 1 },
(_, 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 newDate = setMonth(date, months.indexOf(month));
setDate(newDate);
@ -51,67 +49,170 @@ export function DatePicker({
const handleYearChange = (year: string) => {
const newDate = setYear(date, parseInt(year));
setDate(newDate)
setDate(newDate);
}
const handleSelect = (selectedData: Date | undefined) => {
if (selectedData) {
setDate(selectedData)
const handleHourChange = (hour: string) => {
const newDate = setHours(date, parseInt(hour));
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 (
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-[250px] justify-start text-left font-normal",
"w-[280px] 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>}
{date ? format(date, "PPP") + " " + format(date, "HH:mm:ss") : <span>Pick a date and time</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>
<Tabs defaultValue="date">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="date">
<CalendarIcon className="mr-2 h-4 w-4" />
Date
</TabsTrigger>
<TabsTrigger value="time">
<Clock className="mr-2 h-4 w-4" />
Time
</TabsTrigger>
</TabsList>
<Calendar
mode="single"
selected={date}
onSelect={handleSelect}
initialFocus
month={date}
onMonthChange={setDate}
/>
<TabsContent value="date" className="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}
/>
</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>
</Popover>
)

View File

@ -13,7 +13,7 @@ const ReactQueryProvider = ({ children }: { children: React.ReactNode }) => {
// Pengaturan caching global
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
refetchOnWindowFocus: true,
retry: 1,
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff

View File

@ -106,7 +106,6 @@ export const UpdateUserParamsSchema = z.object({
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