Skip to content

Commit c16c0b9

Browse files
Merge branch 'main' into mobile
2 parents 35e88ae + 7b771a6 commit c16c0b9

File tree

11 files changed

+314
-179
lines changed

11 files changed

+314
-179
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"docker:logs": "docker-compose logs -f app"
1818
},
1919
"dependencies": {
20+
"@conform-to/react": "^1.13.2",
21+
"@conform-to/zod": "^1.13.2",
2022
"@hookform/resolvers": "^5.2.1",
2123
"@icons-pack/react-simple-icons": "^13.8.0",
2224
"@next/bundle-analyzer": "^16.0.1",

pnpm-lock.yaml

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/create-url-dialog.tsx

Lines changed: 69 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
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"
47
import { toast } from "sonner"
58
import { Button } from "@/components/ui/button"
69
import {
@@ -13,6 +16,9 @@ import {
1316
} from "@/components/ui/dialog"
1417
import { Input } from "@/components/ui/input"
1518
import { Label } from "@/components/ui/label"
19+
import { createUrl } from "@/lib/actions"
20+
import { createUrlSchema } from "@/lib/validations"
21+
import { RandomText } from "./random-text"
1622

1723
interface 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>

src/components/dashboard.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { useUrls } from "@/hooks/urls"
3535
import type { UrlRecord, UrlsQueryParams } from "@/lib/schemas"
3636
import { copyToClipboard } from "@/lib/utils"
3737
import { CreateUrlDialog } from "./create-url-dialog"
38-
import { EditUrlDialog } from "./edit-url-dialog"
38+
import { type EditDialogState, EditUrlDialog } from "./edit-url-dialog"
3939
import { PaginationControls } from "./pagination"
4040
import { Toggle } from "./ui/toggle"
4141
import { MobileRow, UrlRecordRow } from "./url-record-row"
@@ -58,10 +58,7 @@ export function Dashboard() {
5858
const { urls, pagination, loading, refetch } = useUrls(queryParams)
5959

6060
const [createDialogOpen, setCreateDialogOpen] = useState(false)
61-
const [editDialog, setEditDialog] = useState<{
62-
open: boolean
63-
url?: UrlRecord
64-
}>({ open: false })
61+
const [editDialog, setEditDialog] = useState<EditDialogState>({ open: false })
6562

6663
const handleCustomOnlyToggle = () => {
6764
setQueryParams((prev) => ({
@@ -300,11 +297,8 @@ export function Dashboard() {
300297
/>
301298

302299
<EditUrlDialog
303-
open={editDialog.open}
304-
url={editDialog.url}
305-
onOpenChange={(open: boolean) =>
306-
setEditDialog({ open, url: undefined })
307-
}
300+
{...editDialog}
301+
onClose={() => setEditDialog({ open: false })}
308302
onSuccess={() => {
309303
refetch()
310304
setEditDialog({ open: false })

0 commit comments

Comments
 (0)