Skip to content

Commit a4db412

Browse files
committed
feat: qr image endpoint
1 parent 3704f97 commit a4db412

20 files changed

+4000
-1317
lines changed

next.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ const nextConfig: NextConfig = {
88
output: "standalone",
99
transpilePackages: ["@t3-oss/env-nextjs", "@t3-oss/env-core"],
1010
experimental: { reactCompiler: true, swcTraceProfiling: ANALYZE_AND_PROFILE },
11+
turbopack: {
12+
rules: {
13+
"*.svg": {
14+
loaders: ["@svgr/webpack"],
15+
as: "*.js",
16+
},
17+
},
18+
},
1119
}
1220

1321
export default withBundleAnalyzer(nextConfig)

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@radix-ui/react-toggle": "^1.1.10",
3232
"@radix-ui/react-tooltip": "^1.2.8",
3333
"@scalar/nextjs-api-reference": "^0.8.23",
34+
"@svgr/webpack": "^8.1.0",
3435
"@t3-oss/env-nextjs": "^0.13.8",
3536
"@tanstack/react-query": "^5.90.5",
3637
"@ts-rest/core": "3.52.1",
@@ -39,8 +40,10 @@
3940
"@ts-rest/serverless": "^3.52.1",
4041
"@types/pg": "^8.15.5",
4142
"ajv": "^8.17.1",
43+
"canvas": "^3.2.0",
4244
"class-variance-authority": "^0.7.1",
4345
"clsx": "^2.1.1",
46+
"jsdom": "^27.1.0",
4447
"lucide-react": "^0.552.0",
4548
"nanoid": "^5.1.5",
4649
"next": "15.5.2",
@@ -58,6 +61,7 @@
5861
"devDependencies": {
5962
"@biomejs/biome": "2.2.0",
6063
"@tailwindcss/postcss": "^4",
64+
"@types/jsdom": "^27.0.0",
6165
"@types/node": "^24.3.1",
6266
"@types/react": "^19",
6367
"@types/react-dom": "^19",
@@ -66,5 +70,5 @@
6670
"tw-animate-css": "^1.3.8",
6771
"typescript": "^5"
6872
},
69-
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
73+
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd"
7074
}

pnpm-lock.yaml

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

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
onlyBuiltDependencies:
22
- '@tailwindcss/oxide'
3+
- canvas
34
- sharp
45
- vue-demi

src/app/api/[...ts-rest]/route.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
import { createNextHandler } from "@ts-rest/serverless/next"
22
import { contract } from "@/lib/contract"
3+
import { generateQR } from "@/lib/qr/server"
34
import { urlService } from "@/lib/url-service"
5+
import { makeShortUrl } from "@/lib/utils"
46

57
const handler = createNextHandler(
68
contract,
79
{
810
getAllUrls: async ({ query }) => {
9-
const result = await urlService.getAllUrls({
10-
page: query.page,
11-
limit: query.limit,
12-
search: query.search,
13-
sortBy: query.sortBy,
14-
sortOrder: query.sortOrder,
15-
customOnly: query.customOnly,
16-
})
11+
const result = await urlService.getAllUrls(query)
1712
return {
1813
status: 200,
1914
body: result,
@@ -52,6 +47,14 @@ const handler = createNextHandler(
5247
body: urlRecord,
5348
}
5449
},
50+
getQR: async ({ params, query }) => {
51+
const record = await urlService.getUrlByShortCode(params.shortCode)
52+
if (!record) return { status: 404, body: { error: "URL not found" } }
53+
54+
const url = makeShortUrl(record)
55+
const body = await generateQR(url, 1024, params.ext, query)
56+
return { status: 200, body }
57+
},
5558
updateUrl: async ({ params, body }) => {
5659
const urlRecord = await urlService.updateUrl(params.shortCode, body.url)
5760
if (!urlRecord) {

src/components/create-url-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import { Input } from "@/components/ui/input"
1818
import { Label } from "@/components/ui/label"
1919
import { env } from "@/env"
20-
import { createUrl } from "@/lib/actions"
20+
import { createUrl } from "@/lib/actions/forms"
2121
import { createUrlSchema } from "@/lib/validations"
2222
import { RandomText } from "./random-text"
2323

src/components/dashboard.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
} from "@/components/ui/table"
3434
import { env } from "@/env"
3535
import { useUrls } from "@/hooks/urls"
36+
import { deleteShortUrl } from "@/lib/actions/urls"
3637
import type { UrlRecord, UrlsQueryParams } from "@/lib/schemas"
3738
import { copyToClipboard, makeShortUrl } from "@/lib/utils"
3839
import { CreateUrlDialog } from "./create-url-dialog"
@@ -97,15 +98,12 @@ export function Dashboard() {
9798
if (!confirm("Are you sure you want to delete this URL?")) return
9899

99100
try {
100-
const response = await fetch(`/api/urls/${shortCode}`, {
101-
method: "DELETE",
102-
})
103-
104-
if (response.ok) {
101+
const success = await deleteShortUrl(shortCode)
102+
if (success) {
105103
toast.success("URL deleted successfully")
106104
refetch()
107105
} else {
108-
toast.error("Failed to delete URL")
106+
toast.error("URL does not exist")
109107
}
110108
} catch (error) {
111109
console.error("Error deleting URL:", error)

src/components/edit-url-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from "@/components/ui/dialog"
1616
import { Input } from "@/components/ui/input"
1717
import { Label } from "@/components/ui/label"
18-
import { editUrl } from "@/lib/actions"
18+
import { editUrl } from "@/lib/actions/forms"
1919
import type { UrlRecord } from "@/lib/schemas"
2020
import { makeShortUrl } from "@/lib/utils"
2121
import { editUrlSchema } from "@/lib/validations"

src/components/qr-code-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
QR_OPTIONS,
88
type QrOptionKey,
99
type QrOptions,
10-
} from "@/lib/qr-config"
10+
} from "@/lib/qr/config"
1111
import type { UrlRecord } from "@/lib/schemas"
1212
import { makeShortUrl } from "@/lib/utils"
1313
import { QrCode } from "./qr-code"

src/components/qr-code.tsx

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
import type { Options } from "qr-code-styling"
44
import QRCodeStyling from "qr-code-styling"
55
import type React from "react"
6-
import { useEffect, useMemo, useRef } from "react"
7-
import logo from "@/assets/logo.svg"
8-
import type { QrOptions } from "@/lib/qr-config"
6+
import { useCallback, useEffect, useMemo, useRef } from "react"
7+
import { makeOptions, type QrOptions } from "@/lib/qr/config"
98
import { cn } from "@/lib/utils"
109

1110
export type QrCodeProps = React.HTMLAttributes<HTMLDivElement> & {
@@ -24,61 +23,20 @@ export function QrCode({
2423
}: QrCodeProps) {
2524
const ref = useRef<HTMLDivElement>(null)
2625

27-
const { style, background } = options
28-
const styled = style === "styled"
29-
30-
const settings = useMemo<Options>(() => {
31-
const color = styled ? "#1156ae" : "#000000"
32-
return {
33-
type: "canvas",
34-
width: size,
35-
height: size,
36-
margin: size / 32,
37-
data: url,
38-
image: styled ? logo.src : undefined,
39-
imageOptions: { margin: size / 64 },
40-
qrOptions: {
41-
typeNumber: 0,
42-
mode: "Byte",
43-
errorCorrectionLevel: styled ? "Q" : "M",
44-
},
45-
dotsOptions: {
46-
color,
47-
type: styled ? "extra-rounded" : "square",
48-
},
49-
cornersDotOptions: {
50-
color,
51-
},
52-
cornersSquareOptions: {
53-
color,
54-
type: styled ? "extra-rounded" : "square",
55-
},
56-
backgroundOptions: {
57-
color: background === "white" ? "#ffffff" : "transparent",
58-
},
59-
}
60-
}, [url, styled, background, size])
26+
// biome-ignore lint/correctness/useExhaustiveDependencies: biome is wrong
27+
const opts = useCallback(makeOptions(options), [options])
28+
const settings = useMemo<Options>(() => opts(url, size), [opts, url, size])
6129

6230
// biome-ignore lint/correctness/useExhaustiveDependencies: manually updated externally
6331
const qr = useMemo(() => new QRCodeStyling(settings), [])
6432

6533
useEffect(() => {
6634
qr.update(settings)
6735
qr.getRawData("png").then((data) => {
68-
if (onImageData) {
69-
let blob: Blob | null = null
70-
if (data instanceof Blob) {
71-
blob = data
72-
} else if (data instanceof Buffer) {
73-
blob = new Blob([new Uint8Array(data)], { type: "image/png" })
74-
}
75-
onImageData(blob)
76-
}
36+
onImageData?.(data instanceof Blob ? data : null)
7737
})
7838
return () => {
79-
if (onImageData) {
80-
onImageData(null)
81-
}
39+
onImageData?.(null)
8240
}
8341
}, [qr, settings, onImageData])
8442

0 commit comments

Comments
 (0)