Skip to content

Commit ed6ea33

Browse files
committed
feat: bump zod v4, time input
1 parent caac40e commit ed6ea33

File tree

14 files changed

+407
-130
lines changed

14 files changed

+407
-130
lines changed

app/(protected)/borrows/[id]/page.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { IsLoggedIn, Verify } from '@/lib/firebase/firebase'
33
import { getBorrow, getListBorrows } from '@/lib/api/borrow'
44
import { Badge } from '@/components/ui/badge'
55
import {
6-
formatDate,
76
getSubscriptionStatus,
87
isBorrowDue,
98
isSubscriptionActive,
@@ -32,6 +31,7 @@ import { Borrow } from '@/lib/types/borrow'
3231
import { Button } from '@/components/ui/button'
3332
import { BtnUndoReturn } from '@/components/borrows/BtnUndoReturn'
3433
import { redirect, RedirectType } from 'next/navigation'
34+
import { DateTime } from '@/components/common/DateTime'
3535

3636
export default async function BorrowDetailsPage({
3737
params,
@@ -168,7 +168,10 @@ export default async function BorrowDetailsPage({
168168
<Calendar className="size-4 text-muted-foreground" />
169169
<p>
170170
<span className="font-medium">Borrowed:&nbsp;</span>
171-
{formatDate(borrowRes.data.borrowed_at)}
171+
<DateTime
172+
dateTime={borrowRes.data.borrowed_at}
173+
relative={!borrowRes.data.returning}
174+
/>
172175
</p>
173176
{isDue ? (
174177
<>
@@ -200,14 +203,17 @@ export default async function BorrowDetailsPage({
200203
)}
201204
<p className={clsx({ 'text-destructive': isDue })}>
202205
<span className="font-medium">Due:&nbsp;</span>
203-
{formatDate(borrowRes.data.due_at)}
206+
<DateTime
207+
dateTime={borrowRes.data.due_at}
208+
relative={!borrowRes.data.returning}
209+
/>
204210
</p>
205211
{borrowRes.data.returning ? (
206212
<>
207213
<CalendarCheck className="size-4 text-muted-foreground" />
208214
<p>
209215
<span className="font-medium">Returned:&nbsp;</span>
210-
{formatDate(borrowRes.data.returning.returned_at)}
216+
<DateTime dateTime={borrowRes.data.returning.returned_at} />
211217
</p>
212218
<Gavel className="size-4 text-muted-foreground" />
213219
<p>
@@ -250,7 +256,7 @@ export default async function BorrowDetailsPage({
250256
<Clock className="size-4" />
251257
<p>
252258
<span className="font-medium">Expires:&nbsp;</span>
253-
{formatDate(borrowRes.data.subscription.expires_at)}
259+
<DateTime dateTime={borrowRes.data.subscription.expires_at} />
254260
</p>
255261
<CalendarClock className="size-4" />
256262
<p>
@@ -275,7 +281,7 @@ export default async function BorrowDetailsPage({
275281
<Calendar className="size-4" />
276282
<p>
277283
<span className="font-medium">Purchased At:&nbsp;</span>
278-
{formatDate(borrowRes.data.subscription.created_at)}
284+
<DateTime dateTime={borrowRes.data.subscription.created_at} />
279285
</p>
280286
</CardContent>
281287
</Card>
@@ -315,7 +321,7 @@ export default async function BorrowDetailsPage({
315321
)}
316322

317323
{(isSuperAdmin || isAdmin || isStaff) && (
318-
<div className="bottom-0 sticky py-2 flex flex-col md:flex-row gap-2 md:gap-4 basis-1/2">
324+
<div className="bottom-0 sticky py-2 grid md:grid-cols-2 gap-2">
319325
{borrowRes.data.returning ? (
320326
<BtnUndoReturn
321327
variant="outline"

app/globals.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,8 @@
147147
@apply bg-background text-foreground;
148148
}
149149
}
150+
151+
input[type='time']::-webkit-calendar-picker-indicator {
152+
filter: invert(1);
153+
cursor: pointer;
154+
}

components/borrows/FormEditBorrow.tsx

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Calendar } from '../ui/calendar'
2121
import { Input } from '../ui/input'
2222
import { useCallback, useTransition } from 'react'
2323
import { updateBorrowAction } from '@/lib/actions/update-borrow'
24+
import { TimeInput } from '../ui/time-input'
2425

2526
const FormSchema = z.object({
2627
id: z.string({
@@ -89,7 +90,10 @@ export const FormEditBorrow: React.FC<{
8990
className={cn(!field.value && 'text-muted-foreground')}
9091
>
9192
{field.value ? (
92-
formatDate(field.value)
93+
formatDate(field.value, {
94+
hour: '2-digit',
95+
minute: '2-digit',
96+
})
9397
) : (
9498
<span>Pick a date</span>
9599
)}
@@ -106,7 +110,25 @@ export const FormEditBorrow: React.FC<{
106110
date > new Date(form.getValues('due_at')) ||
107111
date < new Date('1900-01-01')
108112
}
109-
initialFocus
113+
autoFocus
114+
/>
115+
<Input
116+
className="max-w-max mx-auto mb-2"
117+
type="time"
118+
value={(() => {
119+
const d = new Date(field.value)
120+
const hh = String(d.getHours()).padStart(2, '0')
121+
const mm = String(d.getMinutes()).padStart(2, '0')
122+
return `${hh}:${mm}`
123+
})()}
124+
onChange={(e) => {
125+
const [hh = 0, mm = 0] = e.target.value
126+
.split(':')
127+
.map(Number)
128+
const d = new Date(field.value)
129+
d.setHours(hh, mm)
130+
field.onChange(d.toISOString())
131+
}}
110132
/>
111133
</PopoverContent>
112134
</Popover>
@@ -129,7 +151,10 @@ export const FormEditBorrow: React.FC<{
129151
className={cn(!field.value && 'text-muted-foreground')}
130152
>
131153
{field.value ? (
132-
formatDate(field.value)
154+
formatDate(field.value, {
155+
hour: '2-digit',
156+
minute: '2-digit',
157+
})
133158
) : (
134159
<span>Pick a date</span>
135160
)}
@@ -145,8 +170,9 @@ export const FormEditBorrow: React.FC<{
145170
disabled={(date) =>
146171
date < new Date(form.getValues('borrowed_at'))
147172
}
148-
initialFocus
173+
autoFocus
149174
/>
175+
<TimeInput value={field.value} onChange={field.onChange} />
150176
</PopoverContent>
151177
</Popover>
152178

@@ -172,7 +198,10 @@ export const FormEditBorrow: React.FC<{
172198
)}
173199
>
174200
{field.value ? (
175-
formatDate(field.value)
201+
formatDate(field.value, {
202+
hour: '2-digit',
203+
minute: '2-digit',
204+
})
176205
) : (
177206
<span>Pick a date</span>
178207
)}
@@ -188,7 +217,11 @@ export const FormEditBorrow: React.FC<{
188217
disabled={(date) =>
189218
date < new Date(form.getValues('borrowed_at'))
190219
}
191-
initialFocus
220+
autoFocus
221+
/>
222+
<TimeInput
223+
value={field.value}
224+
onChange={field.onChange}
192225
/>
193226
</PopoverContent>
194227
</Popover>

components/common/DateTime.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { formatDate } from '@/lib/utils'
2+
import { formatDistanceToNowStrict } from 'date-fns'
3+
4+
interface Props extends React.HTMLAttributes<HTMLTimeElement> {
5+
dateTime: string
6+
children?: React.ReactNode
7+
relative?: boolean
8+
}
9+
10+
export function DateTime({ dateTime, children, relative, ...rest }: Props) {
11+
const formatted = formatDate(dateTime, {
12+
hour: '2-digit',
13+
minute: '2-digit',
14+
})
15+
const distance = formatDistanceToNowStrict(new Date(dateTime), {
16+
addSuffix: true,
17+
})
18+
return (
19+
<time dateTime={dateTime.slice(0, 19)} {...rest}>
20+
{children ?? `${formatted}${relative ? ` (${distance})` : ''}`}
21+
</time>
22+
)
23+
}

components/dashboard/DateRangeSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export function DateRangeSelector({
9494
</SelectContent>
9595
</Select>
9696
<Calendar
97-
initialFocus
97+
autoFocus
9898
mode="range"
9999
defaultMonth={date?.from}
100100
selected={date}

components/libraries/LibraryForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { zodResolver } from '@hookform/resolvers/zod'
44
import { useForm } from 'react-hook-form'
5-
import { z } from 'zod'
5+
import { z } from 'zod/v4'
66
import {
77
Form,
88
FormControl,
@@ -28,7 +28,7 @@ type LibraryFormProps = {
2828
}
2929

3030
const FormSchema = z.object({
31-
name: z.string().nonempty({ message: 'Name is required' }),
31+
name: z.string().nonempty({ error: 'Name is required' }),
3232
logo: z.string().optional(),
3333
address: z.string().optional(),
3434
phone: z.string().optional(),

components/ui/avatar.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
"use client"
1+
'use client'
22

3-
import * as React from "react"
4-
import * as AvatarPrimitive from "@radix-ui/react-avatar"
3+
import * as React from 'react'
4+
import * as AvatarPrimitive from '@radix-ui/react-avatar'
55

6-
import { cn } from "@/lib/utils"
6+
import { cn } from '@/lib/utils'
77

88
function Avatar({
99
className,
@@ -13,7 +13,7 @@ function Avatar({
1313
<AvatarPrimitive.Root
1414
data-slot="avatar"
1515
className={cn(
16-
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
16+
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
1717
className
1818
)}
1919
{...props}
@@ -28,7 +28,7 @@ function AvatarImage({
2828
return (
2929
<AvatarPrimitive.Image
3030
data-slot="avatar-image"
31-
className={cn("aspect-square size-full", className)}
31+
className={cn('aspect-square size-full', className)}
3232
{...props}
3333
/>
3434
)
@@ -42,7 +42,7 @@ function AvatarFallback({
4242
<AvatarPrimitive.Fallback
4343
data-slot="avatar-fallback"
4444
className={cn(
45-
"bg-muted flex size-full items-center justify-center rounded-full",
45+
'bg-muted flex size-full items-center justify-center rounded-full',
4646
className
4747
)}
4848
{...props}

components/ui/button.tsx

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,27 @@ import { cva, type VariantProps } from 'class-variance-authority'
55
import { cn } from '@/lib/utils'
66

77
const buttonVariants = cva(
8-
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
8+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
99
{
1010
variants: {
1111
variant: {
12-
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
12+
default:
13+
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
1314
destructive:
14-
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15+
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
1516
outline:
16-
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17+
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
1718
secondary:
18-
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19-
ghost: 'hover:bg-accent hover:text-accent-foreground',
19+
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
20+
ghost:
21+
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
2022
link: 'text-primary underline-offset-4 hover:underline',
2123
},
2224
size: {
23-
default: 'h-10 px-4 py-2',
24-
sm: 'h-9 rounded-md px-3',
25-
lg: 'h-11 rounded-md px-8',
26-
icon: 'h-10 w-10',
25+
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
26+
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
27+
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
28+
icon: 'size-9',
2729
},
2830
},
2931
defaultVariants: {
@@ -33,24 +35,25 @@ const buttonVariants = cva(
3335
}
3436
)
3537

36-
export interface ButtonProps
37-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38-
VariantProps<typeof buttonVariants> {
39-
asChild?: boolean
40-
}
38+
function Button({
39+
className,
40+
variant,
41+
size,
42+
asChild = false,
43+
...props
44+
}: React.ComponentProps<'button'> &
45+
VariantProps<typeof buttonVariants> & {
46+
asChild?: boolean
47+
}) {
48+
const Comp = asChild ? Slot : 'button'
4149

42-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43-
({ className, variant, size, asChild = false, ...props }, ref) => {
44-
const Comp = asChild ? Slot : 'button'
45-
return (
46-
<Comp
47-
className={cn(buttonVariants({ variant, size, className }))}
48-
ref={ref}
49-
{...props}
50-
/>
51-
)
52-
}
53-
)
54-
Button.displayName = 'Button'
50+
return (
51+
<Comp
52+
data-slot="button"
53+
className={cn(buttonVariants({ variant, size, className }))}
54+
{...props}
55+
/>
56+
)
57+
}
5558

5659
export { Button, buttonVariants }

0 commit comments

Comments
 (0)