11"use client"
22
3- import { useState } from "react"
3+ import { getFormProps , getInputProps , useForm } from "@conform-to/react"
4+ import { getZodConstraint , parseWithZod } from "@conform-to/zod"
5+ import { nanoid } from "nanoid"
6+ import { useActionState , useCallback } from "react"
47import { toast } from "sonner"
58import { Button } from "@/components/ui/button"
69import {
@@ -13,6 +16,9 @@ import {
1316} from "@/components/ui/dialog"
1417import { Input } from "@/components/ui/input"
1518import { Label } from "@/components/ui/label"
19+ import { createUrl } from "@/lib/actions"
20+ import { createUrlSchema } from "@/lib/validations"
21+ import { RandomText } from "./random-text"
1622
1723interface CreateUrlDialogProps {
1824 open : boolean
@@ -25,108 +31,105 @@ export function CreateUrlDialog({
2531 onOpenChange,
2632 onSuccess,
2733} : CreateUrlDialogProps ) {
28- const [ url , setUrl ] = useState ( "" )
29- const [ shortCode , setShortCode ] = useState ( "" )
30- const [ loading , setLoading ] = useState ( false )
31-
32- const handleSubmit = async ( e : React . FormEvent ) => {
33- e . preventDefault ( )
34- if ( ! url . trim ( ) ) return
35-
36- setLoading ( true )
37- try {
38- const requestBody : { url : string ; shortCode ?: string } = {
39- url : url . trim ( ) ,
40- }
41-
42- if ( shortCode . trim ( ) ) {
43- requestBody . shortCode = shortCode . trim ( )
34+ const [ { error, lastResult } , action , pending ] = useActionState ( createUrl , {
35+ error : null ,
36+ lastResult : null ,
37+ } )
38+ const [ form , fields ] = useForm ( {
39+ lastResult,
40+ constraint : getZodConstraint ( createUrlSchema ) ,
41+ onValidate : ( { formData } ) =>
42+ parseWithZod ( formData , { schema : createUrlSchema } ) ,
43+ onSubmit : ( ) => {
44+ if ( error ) {
45+ console . error ( "Error creating URL:" , error )
46+ toast . error ( `Error creating URL: ${ error } ` )
47+ } else {
48+ toast . success ( "Short URL created successfully!" )
4449 }
50+ onSuccess ( )
51+ } ,
4552
46- const response = await fetch ( "/api/urls" , {
47- method : "POST" ,
48- headers : {
49- "Content-Type" : "application/json" ,
50- } ,
51- body : JSON . stringify ( requestBody ) ,
52- } )
53+ shouldValidate : "onBlur" ,
54+ shouldRevalidate : "onInput" ,
55+ } )
5356
54- const data = await response . json ( )
55-
56- if ( response . ok ) {
57- toast . success ( `Short URL created: ${ data . short_code } ` )
58- setUrl ( "" )
59- setShortCode ( "" )
60- onSuccess ( )
61- } else {
62- toast . error ( data . error || "Failed to create short URL" )
63- }
64- } catch ( error ) {
65- console . error ( "Error creating URL:" , error )
66- toast . error ( "Failed to create short URL" )
67- } finally {
68- setLoading ( false )
69- }
70- }
57+ const randomCode = useCallback ( ( ) => nanoid ( 8 ) , [ ] )
58+ const isRandom = ! ( fields . shortCode . value && fields . shortCode . valid )
7159
7260 return (
7361 < Dialog open = { open } onOpenChange = { onOpenChange } >
7462 < DialogContent className = "sm:max-w-[425px]" >
7563 < DialogHeader >
7664 < DialogTitle > Create Short URL</ DialogTitle >
7765 < DialogDescription >
78- Enter a URL to create a short URL . Optionally specify a custom short
79- code.
66+ Enter a URL to create a shortened version . Optionally specify a
67+ custom short code.
8068 </ DialogDescription >
8169 </ DialogHeader >
82- < form onSubmit = { handleSubmit } >
83- < div className = "grid gap-4 py-4" >
84- < div className = "grid col-span-4 grid-cols-4 items-center gap-4" >
85- < Label htmlFor = "url" className = "text-right" >
70+ < form { ...getFormProps ( form , { } ) } action = { action } >
71+ < div > { form . errors } </ div >
72+ < div className = "grid grid-cols-4 gap-x-4 py-4" >
73+ < span
74+ id = { fields . url . errorId }
75+ className = "text-xs col-start-2 col-span-3 text-red-600 text-center"
76+ >
77+ { fields . url . errors }
78+ </ span >
79+ < div className = "grid col-span-4 grid-cols-4 items-center gap-4 mb-4" >
80+ < Label htmlFor = { fields . url . id } className = "text-right" >
8681 URL
8782 </ Label >
8883 < Input
89- id = "url"
90- type = "url"
84+ { ...getInputProps ( fields . url , { type : "url" } ) }
9185 placeholder = "https://example.polinetwork.org/path"
92- value = { url }
93- onChange = { ( e ) => setUrl ( e . target . value ) }
9486 className = "col-span-3"
95- required
9687 />
9788 </ div >
98- < div className = "grid col-span-4 grid-cols-4 items-center gap-4" >
99- < Label htmlFor = "shortCode" className = "text-right" >
89+ < span
90+ id = { fields . shortCode . errorId }
91+ className = "text-xs col-start-2 col-span-3 text-red-600 text-center"
92+ >
93+ { fields . shortCode . errors ?. join ( ", " ) }
94+ </ span >
95+ < div className = "grid col-span-4 grid-cols-4 items-center gap-4 mb-4" >
96+ < Label htmlFor = { fields . shortCode . id } className = "text-right" >
10097 Short Code
10198 </ Label >
10299 < Input
103- id = "shortCode"
104- type = "text"
100+ { ...getInputProps ( fields . shortCode , { type : "text" } ) }
105101 placeholder = "custom-code (optional)"
106- value = { shortCode }
107- onChange = { ( e ) => setShortCode ( e . target . value ) }
108102 className = "col-span-3"
109- pattern = "[a-zA-Z0-9_\-]{3,20}"
110- minLength = { 3 }
111- maxLength = { 20 }
112- title = "Short code can only contain letters, numbers, hyphens and underscores (3-20 characters)"
103+ title = "Short code can only contain letters, numbers, hyphens and underscores (2-20 characters)"
113104 />
114105 </ div >
115106 < div className = "col-span-4 text-sm text-muted-foreground" >
116- Leave short code empty to auto-generate a random one.
107+ If you leave < i > Short Code</ i > empty, a random one will be
108+ auto-generated upon submission.
117109 </ div >
118110 </ div >
111+ < p className = "text-xs" > Preview: </ p >
112+ < div className = "text-sm p-4 border rounded-md border-border mb-4 mt-1 flex flex-col gap-1 bg-muted/50 text-muted-foreground" >
113+ < p className = "font-mono mx-auto" >
114+ https://polinet.cc/
115+ { isRandom ? (
116+ < RandomText generate = { randomCode } />
117+ ) : (
118+ < span > { fields . shortCode . value } </ span >
119+ ) }
120+ </ p >
121+ </ div >
119122 < DialogFooter >
120123 < Button
121124 type = "button"
122125 variant = "outline"
123126 onClick = { ( ) => onOpenChange ( false ) }
124- disabled = { loading }
127+ disabled = { pending }
125128 >
126129 Cancel
127130 </ Button >
128- < Button type = "submit" disabled = { loading || ! url . trim ( ) } >
129- { loading ? "Creating..." : "Create" }
131+ < Button type = "submit" disabled = { pending } >
132+ { pending ? "Creating..." : "Create" }
130133 </ Button >
131134 </ DialogFooter >
132135 </ form >
0 commit comments