Skip to content

Commit f02d11a

Browse files
implement image upload and SVG conversion using Cloudinary and ImageTracer
1 parent 2628d89 commit f02d11a

File tree

5 files changed

+344
-98
lines changed

5 files changed

+344
-98
lines changed

app/api/upload/route.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { v2 as cloudinary, UploadApiResponse } from "cloudinary";
2+
// @ts-ignore
3+
import ImageTracer from "imagetracerjs";
4+
import { NextResponse } from "next/server";
5+
import fetch from "node-fetch";
6+
import { PNG } from "pngjs";
7+
8+
cloudinary.config({
9+
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
10+
api_key: process.env.CLOUDINARY_API_KEY!,
11+
api_secret: process.env.CLOUDINARY_API_SECRET!,
12+
});
13+
14+
async function cloudinaryToSVG(imageUrl: string, options = { scale: 5 }) {
15+
const response = await fetch(imageUrl);
16+
if (!response.ok)
17+
throw new Error(`Failed to fetch image: ${response.statusText}`);
18+
const buffer = Buffer.from(await response.arrayBuffer());
19+
20+
const png = PNG.sync.read(buffer);
21+
22+
const myImageData = {
23+
width: png.width,
24+
height: png.height,
25+
data: png.data,
26+
};
27+
28+
const svgstring = ImageTracer.imagedataToSVG(myImageData, options);
29+
return svgstring;
30+
}
31+
32+
export async function POST(req: Request) {
33+
const formData = await req.formData();
34+
const file = formData.get("image") as File;
35+
36+
if (!file) {
37+
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
38+
}
39+
40+
// Read file as buffer
41+
const bytes = await file.arrayBuffer();
42+
const buffer = Buffer.from(bytes);
43+
44+
const cloudinaryResponse: UploadApiResponse = await new Promise(
45+
(resolve, reject) => {
46+
const uploadStream = cloudinary.uploader.upload_stream((err, result) => {
47+
if (err || !result) return reject(err);
48+
resolve(result);
49+
});
50+
uploadStream.end(buffer);
51+
}
52+
);
53+
54+
const svgString = await cloudinaryToSVG(cloudinaryResponse.secure_url);
55+
56+
return NextResponse.json(
57+
{ url: cloudinaryResponse.secure_url, svg: svgString },
58+
{ status: 201 }
59+
);
60+
}

app/api/users/route.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export async function GET(request: Request) {
2+
// For example, fetch data from your DB here
3+
const users = [
4+
{ id: 1, name: "Alice" },
5+
{ id: 2, name: "Bob" },
6+
];
7+
return new Response(JSON.stringify(users), {
8+
status: 200,
9+
headers: { "Content-Type": "application/json" },
10+
});
11+
}
12+
13+
export async function POST(request: Request) {
14+
// Parse the request body
15+
const body = await request.json();
16+
const { name } = body;
17+
18+
// e.g. Insert new user into your DB
19+
const newUser = { id: Date.now(), name };
20+
21+
return new Response(JSON.stringify(newUser), {
22+
status: 201,
23+
headers: { "Content-Type": "application/json" },
24+
});
25+
}

app/page.tsx

Lines changed: 33 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,38 @@
1-
import Image from "next/image";
1+
"use client";
22

3-
export default function Home() {
4-
return (
5-
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
6-
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
7-
<Image
8-
className="dark:invert"
9-
src="/next.svg"
10-
alt="Next.js logo"
11-
width={180}
12-
height={38}
13-
priority
14-
/>
15-
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
16-
<li className="mb-2 tracking-[-.01em]">
17-
Get started by editing{" "}
18-
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
19-
app/page.tsx
20-
</code>
21-
.
22-
</li>
23-
<li className="tracking-[-.01em]">
24-
Save and see your changes instantly.
25-
</li>
26-
</ol>
3+
import { useState } from "react";
4+
5+
export default function UploadPage() {
6+
const [file, setFile] = useState<File | null>(null);
7+
const [data, setData] = useState<String | null>(null);
8+
9+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
10+
if (e.target.files) {
11+
setFile(e.target.files[0]);
12+
}
13+
};
14+
15+
const handleUpload = async () => {
16+
if (!file) return;
2717

28-
<div className="flex gap-4 items-center flex-col sm:flex-row">
29-
<a
30-
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
31-
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
32-
target="_blank"
33-
rel="noopener noreferrer"
34-
>
35-
<Image
36-
className="dark:invert"
37-
src="/vercel.svg"
38-
alt="Vercel logomark"
39-
width={20}
40-
height={20}
41-
/>
42-
Deploy now
43-
</a>
44-
<a
45-
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
46-
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
47-
target="_blank"
48-
rel="noopener noreferrer"
49-
>
50-
Read our docs
51-
</a>
52-
</div>
53-
</main>
54-
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
55-
<a
56-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
57-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
58-
target="_blank"
59-
rel="noopener noreferrer"
60-
>
61-
<Image
62-
aria-hidden
63-
src="/file.svg"
64-
alt="File icon"
65-
width={16}
66-
height={16}
67-
/>
68-
Learn
69-
</a>
70-
<a
71-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
72-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
73-
target="_blank"
74-
rel="noopener noreferrer"
75-
>
76-
<Image
77-
aria-hidden
78-
src="/window.svg"
79-
alt="Window icon"
80-
width={16}
81-
height={16}
82-
/>
83-
Examples
84-
</a>
85-
<a
86-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
87-
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
88-
target="_blank"
89-
rel="noopener noreferrer"
90-
>
91-
<Image
92-
aria-hidden
93-
src="/globe.svg"
94-
alt="Globe icon"
95-
width={16}
96-
height={16}
97-
/>
98-
Go to nextjs.org →
99-
</a>
100-
</footer>
18+
const formData = new FormData();
19+
formData.append("image", file);
20+
21+
const res = await fetch("/api/upload", {
22+
method: "POST",
23+
body: formData,
24+
});
25+
26+
const data = await res.json();
27+
setData(data.url + "\n\n" + data.svg);
28+
console.log("Response:", data);
29+
};
30+
31+
return (
32+
<div>
33+
<input type="file" accept="image/*" onChange={handleFileChange} />
34+
<button onClick={handleUpload}>Upload</button>
35+
<p>{data}</p>
10136
</div>
10237
);
10338
}

0 commit comments

Comments
 (0)