Skip to content

Commit 3681d2f

Browse files
Merge pull request #17 from Shitanshukumar607/portrace
add preview images
2 parents 8d121e4 + cebd422 commit 3681d2f

File tree

16 files changed

+896
-174
lines changed

16 files changed

+896
-174
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Users can **preview**, **copy the SVG code**, or **download the file** to use an
1919
## 🛠️ Tech Stack
2020

2121
- **Frontend:** React / Next.js
22-
- **Vectorization:** [ImageTracer.js](https://github.com/jankovicsandras/imagetracerjs)
22+
- **Vectorization:** [Potrace](https://github.com/tooolbox/node-potrace)
2323
- **Styling:** TailwindCSS (or your chosen framework)
2424

2525
---

app/api/upload/route.ts

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,30 @@
11
import { v2 as cloudinary, UploadApiResponse } from "cloudinary";
2-
// @ts-expect-error: imagetracerjs has no TypeScript types
3-
import ImageTracer from "imagetracerjs";
42
import { NextResponse } from "next/server";
5-
import fetch from "node-fetch";
6-
import sharp from "sharp";
3+
4+
import { createRequire } from "module";
5+
const require = createRequire(import.meta.url);
6+
const potrace = require("potrace");
77

88
cloudinary.config({
99
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
1010
api_key: process.env.CLOUDINARY_API_KEY!,
1111
api_secret: process.env.CLOUDINARY_API_SECRET!,
1212
});
1313

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 image = sharp(buffer).ensureAlpha(); // ensures RGBA format
21-
const { width, height } = await image.metadata();
22-
23-
if (!width || !height) {
24-
throw new Error("Unable to read image metadata.");
25-
}
26-
27-
const raw = await image.raw().toBuffer();
28-
29-
const myImageData = {
30-
width,
31-
height,
32-
data: raw,
33-
};
14+
async function cloudinaryToSVG(imageUrl: string) {
15+
const svgString: string = await new Promise((resolve, reject) => {
16+
potrace.posterize(imageUrl, (err: unknown, svg: string) => {
17+
if (err)
18+
return reject(
19+
new Error(
20+
`Failed to convert image to SVG: ${(err as Error)?.message}`,
21+
),
22+
);
23+
resolve(svg);
24+
});
25+
});
3426

35-
const svgstring = ImageTracer.imagedataToSVG(myImageData, options);
36-
return svgstring;
27+
return svgString;
3728
}
3829

3930
export async function POST(req: Request) {
@@ -73,8 +64,7 @@ export async function POST(req: Request) {
7364
);
7465
} catch (err: unknown) {
7566
console.error("Upload error:", err);
76-
const errorMessage =
77-
err instanceof Error ? err.message : "Internal Server Error";
67+
const errorMessage = (err as Error)?.message || "Internal Server Error";
7868
return NextResponse.json(
7969
{ success: false, error: errorMessage },
8070
{ status: 500 },

app/api/users/route.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

app/api/works/route.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export async function GET(request: NextRequest) {
1010

1111
const supabase = await createClient();
1212

13-
// Get the current user
1413
const {
1514
data: { user },
1615
error: authError,
@@ -20,7 +19,6 @@ export async function GET(request: NextRequest) {
2019
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
2120
}
2221

23-
// Fetch works for the current user
2422
const { data: works, error: worksError } = await supabase
2523
.from("works")
2624
.select("*")
@@ -36,7 +34,6 @@ export async function GET(request: NextRequest) {
3634
);
3735
}
3836

39-
// Get total count for pagination
4037
const { count, error: countError } = await supabase
4138
.from("works")
4239
.select("*", { count: "exact", head: true })

app/generate/page.tsx

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export default function UploadPage() {
166166
updateProgress(100);
167167

168168
if (data.success && data.svg) {
169-
setSvgResult(fixSvg(data.svg));
169+
setSvgResult(data.svg);
170170
} else {
171171
setError(data.error || "Failed to convert image to SVG");
172172
}
@@ -215,32 +215,6 @@ export default function UploadPage() {
215215
URL.revokeObjectURL(url);
216216
};
217217

218-
const fixSvg = (svg: string) => {
219-
// get width/height
220-
const widthMatch = svg.match(/width="(\d+)"/);
221-
const heightMatch = svg.match(/height="(\d+)"/);
222-
223-
if (widthMatch && heightMatch) {
224-
const width = widthMatch[1];
225-
const height = heightMatch[1];
226-
227-
// make width/height to 100%
228-
svg = svg
229-
.replace(/width="[^"]*"/, 'width="100%"')
230-
.replace(/height="[^"]*"/, 'height="100%"');
231-
232-
// add viewBox if missing
233-
if (!/viewBox=/.test(svg)) {
234-
svg = svg.replace(
235-
/<svg([^>]*)>/,
236-
`<svg$1 viewBox="0 0 ${width} ${height}">`,
237-
);
238-
}
239-
}
240-
241-
return svg;
242-
};
243-
244218
return (
245219
<section className="relative overflow-hidden bg-zinc-50 dark:bg-neutral-900/80 min-h-screen py-12 px-4">
246220
{/* Accent Corner Lines */}

components/Collections/WorkCard.tsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,6 @@ export function WorkCard({ work, onDownload, onView }: WorkCardProps) {
6060
/>
6161
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-black/2 opacity-0 group-hover:opacity-30 transition-opacity" />
6262

63-
{/* Status badges */}
64-
<div className="absolute top-3 left-3 flex gap-2">
65-
{work.svg_data && (
66-
<Badge className="text-xs bg-emerald-100 dark:bg-violet-900/30 text-emerald-700 dark:text-violet-300 border-0 px-2 py-1">
67-
<Code className="mr-1 h-3 w-3" />
68-
Ready
69-
</Badge>
70-
)}
71-
</div>
72-
7363
{/* Action buttons - visible on hover */}
7464
<div className="absolute top-3 right-3 flex gap-2 opacity-0 group-hover:opacity-100 transition-all duration-200">
7565
<Button

components/Collections/WorkViewer.tsx

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -65,32 +65,6 @@ export function WorkViewer({
6565
}
6666
};
6767

68-
const fixSvg = (svg: string) => {
69-
// get width/height
70-
const widthMatch = svg.match(/width="(\d+)"/);
71-
const heightMatch = svg.match(/height="(\d+)"/);
72-
73-
if (widthMatch && heightMatch) {
74-
const width = widthMatch[1];
75-
const height = heightMatch[1];
76-
77-
// make width/height to 100%
78-
svg = svg
79-
.replace(/width="[^"]*"/, 'width="100%"')
80-
.replace(/height="[^"]*"/, 'height="100%"');
81-
82-
// add viewBox if missing
83-
if (!/viewBox=/.test(svg)) {
84-
svg = svg.replace(
85-
/<svg([^>]*)>/,
86-
`<svg$1 viewBox="0 0 ${width} ${height}">`,
87-
);
88-
}
89-
}
90-
91-
return svg;
92-
};
93-
9468
return (
9569
<AnimatePresence>
9670
{isOpen && (
@@ -187,7 +161,7 @@ export function WorkViewer({
187161
<div
188162
className="w-full h-full p-2 flex items-center justify-center"
189163
dangerouslySetInnerHTML={{
190-
__html: fixSvg(work.svg_data),
164+
__html: work.svg_data,
191165
}}
192166
/>
193167
) : (

components/Home/PreviewSection.tsx

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Image from "next/image";
2+
13
const PreviewSection = () => {
24
return (
35
<section className="relative z-10 mt-16 w-full mb-5">
@@ -16,45 +18,39 @@ const PreviewSection = () => {
1618
</div>
1719

1820
<div className="grid gap-6 sm:grid-cols-2">
19-
<div className="flex flex-col gap-4 rounded-2xl border border-slate-200 bg-zinc-50/80 p-5 dark:border-neutral-800 dark:bg-neutral-900/50">
20-
<div className="aspect-[4/3] w-full rounded-2xl bg-gradient-to-br from-emerald-100 to-white ring-1 ring-emerald-200 dark:from-violet-900/30 dark:to-purple-900/60 dark:ring-violet-500/40">
21-
<div className="flex h-full flex-col items-center justify-center gap-2 text-slate-500 dark:text-neutral-400">
22-
<span className="text-lg font-semibold text-slate-600 dark:text-neutral-100">
23-
Input Placeholder
24-
</span>
25-
<span className="text-xs uppercase tracking-[0.3em]">
26-
PNG / JPG
27-
</span>
21+
<div className="flex flex-col gap-4 rounded-2xl border border-slate-200 p-5 dark:border-neutral-800">
22+
<div className="relative aspect-[4/3] w-full rounded-2xl shadow-inner shadow-black/30">
23+
<div className="flex h-full flex-col items-center justify-center gap-2">
24+
<Image
25+
src="/input.jpg"
26+
alt="Input Preview"
27+
fill
28+
className="object-contain bg-white"
29+
/>
2830
</div>
2931
</div>
30-
<div className="space-y-1 text-sm text-slate-600 dark:text-neutral-400">
32+
<div className="flex align-center justify-center space-y-1 text-sm text-slate-600 dark:text-neutral-400">
3133
<p className="font-semibold text-slate-900 dark:text-neutral-100">
32-
Raster Image
33-
</p>
34-
<p className="text-xs text-slate-500 dark:text-neutral-500">
35-
This area will show the file you upload before conversion.
34+
PNG Image
3635
</p>
3736
</div>
3837
</div>
3938

40-
<div className="flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white/80 p-5 dark:border-neutral-800 dark:bg-neutral-900/60">
41-
<div className="aspect-[4/3] w-full rounded-2xl bg-gradient-to-br from-slate-900 to-zinc-900 shadow-inner shadow-black/30">
42-
<div className="flex h-full flex-col items-center justify-center gap-2 text-emerald-400">
43-
<span className="text-lg font-semibold">
44-
Output Placeholder
45-
</span>
46-
<span className="text-xs uppercase tracking-[0.3em]">
47-
SVG Vector
48-
</span>
39+
<div className="flex flex-col gap-4 rounded-2xl border border-slate-200 p-5 dark:border-neutral-800">
40+
<div className="relative aspect-[4/3] w-full rounded-2xl shadow-inner shadow-black/30">
41+
<div className="flex h-full flex-col items-center justify-center gap-2">
42+
<Image
43+
src="/output.svg"
44+
alt="Output Preview"
45+
fill
46+
className="object-contain bg-white"
47+
/>
4948
</div>
5049
</div>
51-
<div className="space-y-1 text-sm text-slate-600 dark:text-neutral-400">
50+
<div className="flex align-center justify-center space-y-1 text-sm text-slate-600 dark:text-neutral-400">
5251
<p className="font-semibold text-slate-900 dark:text-neutral-100">
5352
Editable SVG
5453
</p>
55-
<p className="text-xs text-slate-500 dark:text-neutral-500">
56-
The converted vector will appear here with clean paths.
57-
</p>
5854
</div>
5955
</div>
6056
</div>

0 commit comments

Comments
 (0)