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 { 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" />
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
@ -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]}
|
||||
<SelectContent className="h-[200px] overflow-auto">
|
||||
{years.map((yearValue) => (
|
||||
<SelectItem key={yearValue} value={yearValue.toString()} className="text-sm">
|
||||
{yearValue}
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
@ -414,4 +361,3 @@ export function DateTimePicker({
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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,30 +49,64 @@ 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">
|
||||
<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>
|
||||
|
||||
<TabsContent value="date" className="p-0">
|
||||
<div className="flex justify-between p-2">
|
||||
<Select
|
||||
onValueChange={handleMonthChange}
|
||||
|
@ -112,6 +144,75 @@ export function DatePicker({
|
|||
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>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue