Skip to content

Commit 3ab118e

Browse files
committed
Initial commit
0 parents  commit 3ab118e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+13152
-0
lines changed

.eslintrc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}

.gitignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env*.local
29+
30+
# vercel
31+
.vercel
32+
33+
# typescript
34+
*.tsbuildinfo
35+
next-env.d.ts

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# next-image-watermark
2+
3+
[Edit in StackBlitz next generation editor ⚡️](https://stackblitz.com/~/github.com/hkint/next-image-watermark)

app/globals.css

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
:root {
6+
--foreground-rgb: 0, 0, 0;
7+
--background-start-rgb: 214, 219, 220;
8+
--background-end-rgb: 255, 255, 255;
9+
}
10+
11+
@media (prefers-color-scheme: dark) {
12+
:root {
13+
--foreground-rgb: 255, 255, 255;
14+
--background-start-rgb: 0, 0, 0;
15+
--background-end-rgb: 0, 0, 0;
16+
}
17+
}
18+
19+
@layer base {
20+
:root {
21+
--background: 0 0% 100%;
22+
--foreground: 0 0% 3.9%;
23+
--card: 0 0% 100%;
24+
--card-foreground: 0 0% 3.9%;
25+
--popover: 0 0% 100%;
26+
--popover-foreground: 0 0% 3.9%;
27+
--primary: 0 0% 9%;
28+
--primary-foreground: 0 0% 98%;
29+
--secondary: 0 0% 96.1%;
30+
--secondary-foreground: 0 0% 9%;
31+
--muted: 0 0% 96.1%;
32+
--muted-foreground: 0 0% 45.1%;
33+
--accent: 0 0% 96.1%;
34+
--accent-foreground: 0 0% 9%;
35+
--destructive: 0 84.2% 60.2%;
36+
--destructive-foreground: 0 0% 98%;
37+
--border: 0 0% 89.8%;
38+
--input: 0 0% 89.8%;
39+
--ring: 0 0% 3.9%;
40+
--chart-1: 12 76% 61%;
41+
--chart-2: 173 58% 39%;
42+
--chart-3: 197 37% 24%;
43+
--chart-4: 43 74% 66%;
44+
--chart-5: 27 87% 67%;
45+
--radius: 0.5rem;
46+
}
47+
.dark {
48+
--background: 0 0% 3.9%;
49+
--foreground: 0 0% 98%;
50+
--card: 0 0% 3.9%;
51+
--card-foreground: 0 0% 98%;
52+
--popover: 0 0% 3.9%;
53+
--popover-foreground: 0 0% 98%;
54+
--primary: 0 0% 98%;
55+
--primary-foreground: 0 0% 9%;
56+
--secondary: 0 0% 14.9%;
57+
--secondary-foreground: 0 0% 98%;
58+
--muted: 0 0% 14.9%;
59+
--muted-foreground: 0 0% 63.9%;
60+
--accent: 0 0% 14.9%;
61+
--accent-foreground: 0 0% 98%;
62+
--destructive: 0 62.8% 30.6%;
63+
--destructive-foreground: 0 0% 98%;
64+
--border: 0 0% 14.9%;
65+
--input: 0 0% 14.9%;
66+
--ring: 0 0% 83.1%;
67+
--chart-1: 220 70% 50%;
68+
--chart-2: 160 60% 45%;
69+
--chart-3: 30 80% 55%;
70+
--chart-4: 280 65% 60%;
71+
--chart-5: 340 75% 55%;
72+
}
73+
}
74+
75+
@layer base {
76+
* {
77+
@apply border-border;
78+
}
79+
body {
80+
@apply bg-background text-foreground;
81+
}
82+
}

app/layout.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import './globals.css';
2+
import type { Metadata } from 'next';
3+
import { Inter } from 'next/font/google';
4+
import { Toaster } from 'sonner';
5+
6+
const inter = Inter({ subsets: ['latin'] });
7+
8+
export const metadata: Metadata = {
9+
title: 'Image Watermark - Add Text Watermarks to Images',
10+
description: 'A simple tool to add text watermarks to your image',
11+
};
12+
13+
export default function RootLayout({
14+
children,
15+
}: {
16+
children: React.ReactNode;
17+
}) {
18+
return (
19+
<html lang="en">
20+
<body className={inter.className}>
21+
{children}
22+
<Toaster position="top-center" expand={false} richColors />
23+
</body>
24+
</html>
25+
);
26+
}

app/page.tsx

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
'use client';
2+
3+
import { useState, useEffect, useRef } from 'react';
4+
import { ImageUploader } from '@/components/ImageUploader';
5+
import { WatermarkControls } from '@/components/WatermarkControls';
6+
import { ImagePreview } from '@/components/ImagePreview';
7+
import { PageHeader } from '@/components/PageHeader';
8+
import { PageFooter } from '@/components/PageFooter';
9+
import { HeroContent } from '@/components/HeroContent';
10+
import { FooterContent } from '@/components/FooterContent';
11+
import { WatermarkPosition } from '@/types/watermark';
12+
import { processWatermark } from '@/lib/watermark';
13+
14+
export default function Home() {
15+
const [image, setImage] = useState<string>('');
16+
const [processedImage, setProcessedImage] = useState<string>('');
17+
const [watermark, setWatermark] = useState<string>('👋 Hello');
18+
const [position, setPosition] = useState<WatermarkPosition>('tile');
19+
const [color, setColor] = useState<string>('#334155');
20+
const [fontSize, setFontSize] = useState<number[]>([32]);
21+
const [opacity, setOpacity] = useState<number[]>([45]);
22+
const [rotation, setRotation] = useState<number[]>([360]);
23+
const [watermarkGrid, setWatermarkGrid] = useState<number[]>([6]);
24+
const [font, setFont] = useState<string>('Inter');
25+
26+
const watermarkControlsRef = useRef<HTMLDivElement>(null);
27+
28+
const handleImageUploaded = () => {
29+
setTimeout(() => {
30+
watermarkControlsRef.current?.scrollIntoView({
31+
behavior: 'smooth',
32+
block: 'start',
33+
});
34+
}, 100);
35+
};
36+
37+
useEffect(() => {
38+
const canvas = document.createElement('canvas');
39+
const ctx = canvas.getContext('2d');
40+
if (!ctx) return;
41+
42+
if (!image) {
43+
canvas.width = 1920;
44+
canvas.height = 1080;
45+
46+
ctx.fillStyle = '#ffffff';
47+
ctx.fillRect(0, 0, canvas.width, canvas.height);
48+
49+
ctx.fillStyle = color;
50+
ctx.font = `${fontSize[0]}px ${font}`;
51+
ctx.globalAlpha = opacity[0] / 100;
52+
53+
const addWatermark = (x: number, y: number, rotation: number) => {
54+
ctx.save();
55+
ctx.translate(x, y);
56+
ctx.rotate((rotation * Math.PI) / 180);
57+
ctx.fillText(watermark, 0, 0);
58+
ctx.restore();
59+
};
60+
61+
const metrics = ctx.measureText(watermark);
62+
const textHeight = fontSize[0];
63+
const gridSize = watermarkGrid[0];
64+
const padding = 20;
65+
66+
if (position === 'tile') {
67+
const spacingX = canvas.width / gridSize;
68+
const spacingY = canvas.height / gridSize;
69+
70+
for (let i = 0; i < gridSize; i++) {
71+
for (let j = 0; j < gridSize; j++) {
72+
const x = (i + 0.5) * spacingX - metrics.width / 2;
73+
const y = (j + 0.5) * spacingY + textHeight / 2;
74+
addWatermark(x, y, rotation[0]);
75+
}
76+
}
77+
} else {
78+
const basePositions = {
79+
center: { x: canvas.width / 2, y: canvas.height / 2 },
80+
topCenter: { x: canvas.width / 2, y: padding + textHeight },
81+
bottomCenter: { x: canvas.width / 2, y: canvas.height - padding },
82+
topLeft: { x: padding, y: padding + textHeight },
83+
topRight: { x: canvas.width - padding, y: padding + textHeight },
84+
bottomLeft: { x: padding, y: canvas.height - padding },
85+
bottomRight: {
86+
x: canvas.width - padding,
87+
y: canvas.height - padding,
88+
},
89+
};
90+
91+
const basePos = basePositions[position];
92+
const offsetX = position.includes('Right')
93+
? -metrics.width
94+
: position.toLowerCase().includes('center')
95+
? -metrics.width / 2
96+
: 0;
97+
98+
for (let i = 0; i < gridSize; i++) {
99+
const x =
100+
basePos.x +
101+
offsetX +
102+
(i - Math.floor(gridSize / 2)) * (metrics.width + padding);
103+
addWatermark(x, basePos.y, rotation[0]);
104+
}
105+
}
106+
107+
setProcessedImage(canvas.toDataURL());
108+
} else {
109+
processWatermark({
110+
imageUrl: image,
111+
watermark,
112+
position,
113+
color,
114+
fontSize: fontSize[0],
115+
opacity: opacity[0],
116+
rotation: rotation[0],
117+
watermarkGrid: watermarkGrid[0],
118+
font,
119+
onProcessed: setProcessedImage,
120+
});
121+
}
122+
}, [
123+
image,
124+
watermark,
125+
position,
126+
color,
127+
fontSize,
128+
opacity,
129+
rotation,
130+
watermarkGrid,
131+
font,
132+
]);
133+
134+
return (
135+
<div className="min-h-screen flex flex-col bg-gradient-to-b from-blue-50/50 to-white">
136+
<PageHeader />
137+
138+
<main className="flex-grow px-6 py-4 lg:px-8">
139+
<div className="max-w-7xl mx-auto space-y-4">
140+
<ImageUploader
141+
image={image}
142+
onImageChange={setImage}
143+
onClear={() => {
144+
setImage('');
145+
}}
146+
onImageUploaded={handleImageUploaded}
147+
/>
148+
<HeroContent />
149+
150+
<div ref={watermarkControlsRef} className="space-y-4 p-6">
151+
<WatermarkControls
152+
watermark={watermark}
153+
position={position}
154+
color={color}
155+
fontSize={fontSize}
156+
opacity={opacity}
157+
rotation={rotation}
158+
watermarkGrid={watermarkGrid}
159+
font={font}
160+
onWatermarkChange={(e) => setWatermark(e.target.value)}
161+
onPositionChange={setPosition}
162+
onColorChange={setColor}
163+
onFontSizeChange={setFontSize}
164+
onOpacityChange={setOpacity}
165+
onRotationChange={setRotation}
166+
onWatermarkGridChange={setWatermarkGrid}
167+
onFontChange={setFont}
168+
/>
169+
<ImagePreview processedImage={processedImage} />
170+
</div>
171+
172+
<FooterContent />
173+
</div>
174+
</main>
175+
176+
<PageFooter />
177+
</div>
178+
);
179+
}

components.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "default",
4+
"rsc": true,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "tailwind.config.ts",
8+
"css": "app/globals.css",
9+
"baseColor": "neutral",
10+
"cssVariables": true,
11+
"prefix": ""
12+
},
13+
"aliases": {
14+
"components": "@/components",
15+
"utils": "@/lib/utils",
16+
"ui": "@/components/ui",
17+
"lib": "@/lib",
18+
"hooks": "@/hooks"
19+
}
20+
}

0 commit comments

Comments
 (0)