[dyad] Create image export application - wrote 4 file(s)
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -13,8 +15,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Image Web Exporter",
|
||||||
description: "Generated by create next app",
|
description: "Upload a picture, then export it in a different resolution and format.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -23,11 +25,19 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
<Toaster />
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
|
import { ImageConverter } from "@/components/image-converter";
|
||||||
import { MadeWithDyad } from "@/components/made-with-dyad";
|
import { MadeWithDyad } from "@/components/made-with-dyad";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-rows-[1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
<div className="relative flex flex-col items-center justify-center min-h-screen p-4 bg-gray-50 dark:bg-background font-[family-name:var(--font-geist-sans)]">
|
||||||
<main className="flex flex-col gap-8 row-start-1 items-center sm:items-start">
|
<main className="flex flex-col items-center w-full max-w-2xl z-10">
|
||||||
<h1>Blank page</h1>
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl">
|
||||||
|
Image Web Exporter
|
||||||
|
</h1>
|
||||||
|
<p className="mt-3 text-lg text-gray-600 dark:text-gray-400">
|
||||||
|
Upload a picture, then export it in a different resolution and format.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ImageConverter />
|
||||||
</main>
|
</main>
|
||||||
|
<div className="absolute bottom-4 z-10">
|
||||||
<MadeWithDyad />
|
<MadeWithDyad />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
190
src/components/image-converter.tsx
Normal file
190
src/components/image-converter.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useRef, ChangeEvent } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Upload, Download, Image as ImageIcon } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export function ImageConverter() {
|
||||||
|
const [image, setImage] = useState<File | null>(null);
|
||||||
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
|
const [width, setWidth] = useState<number | string>("");
|
||||||
|
const [height, setHeight] = useState<number | string>("");
|
||||||
|
const [format, setFormat] = useState<"png" | "jpeg" | "webp">("png");
|
||||||
|
const [isConverting, setIsConverting] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file && file.type.startsWith("image/")) {
|
||||||
|
setImage(file);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
setWidth(img.width);
|
||||||
|
setHeight(img.height);
|
||||||
|
};
|
||||||
|
img.src = event.target?.result as string;
|
||||||
|
setPreviewUrl(URL.createObjectURL(file));
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
} else {
|
||||||
|
toast.error("Please select a valid image file.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConvertAndDownload = () => {
|
||||||
|
if (!image || !previewUrl || !width || !height) {
|
||||||
|
toast.error("Please upload an image and set dimensions.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsConverting(true);
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = "anonymous";
|
||||||
|
img.src = previewUrl;
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = Number(width);
|
||||||
|
canvas.height = Number(height);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
if (ctx) {
|
||||||
|
ctx.drawImage(img, 0, 0, Number(width), Number(height));
|
||||||
|
const dataUrl = canvas.toDataURL(`image/${format}`);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = dataUrl;
|
||||||
|
const originalName = image.name.substring(0, image.name.lastIndexOf('.'));
|
||||||
|
link.download = `${originalName}_${width}x${height}.${format}`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
toast.success("Image exported successfully!");
|
||||||
|
} else {
|
||||||
|
toast.error("Could not process the image.");
|
||||||
|
}
|
||||||
|
setIsConverting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
toast.error("Failed to load image for conversion.");
|
||||||
|
setIsConverting(false);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Image Settings</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Adjust the resolution and format for your export.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{previewUrl ? (
|
||||||
|
<div className="w-full aspect-video rounded-md overflow-hidden border flex items-center justify-center bg-gray-100 dark:bg-gray-800">
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt="Image preview"
|
||||||
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="w-full aspect-video rounded-md border-2 border-dashed flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<ImageIcon className="w-12 h-12 mb-2" />
|
||||||
|
<span>Click to upload an image</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleImageChange}
|
||||||
|
className="hidden"
|
||||||
|
accept="image/*"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="width">Width (px)</Label>
|
||||||
|
<Input
|
||||||
|
id="width"
|
||||||
|
type="number"
|
||||||
|
placeholder="e.g., 1920"
|
||||||
|
value={width}
|
||||||
|
onChange={(e) => setWidth(e.target.value)}
|
||||||
|
disabled={!image}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="height">Height (px)</Label>
|
||||||
|
<Input
|
||||||
|
id="height"
|
||||||
|
type="number"
|
||||||
|
placeholder="e.g., 1080"
|
||||||
|
value={height}
|
||||||
|
onChange={(e) => setHeight(e.target.value)}
|
||||||
|
disabled={!image}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="format">Format</Label>
|
||||||
|
<Select
|
||||||
|
value={format}
|
||||||
|
onValueChange={(value: "png" | "jpeg" | "webp") => setFormat(value)}
|
||||||
|
disabled={!image}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="format">
|
||||||
|
<SelectValue placeholder="Select format" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="png">PNG</SelectItem>
|
||||||
|
<SelectItem value="jpeg">JPEG</SelectItem>
|
||||||
|
<SelectItem value="webp">WEBP</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex justify-between">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<Upload className="mr-2 h-4 w-4" />
|
||||||
|
{image ? "Change Image" : "Upload Image"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleConvertAndDownload}
|
||||||
|
disabled={!image || isConverting}
|
||||||
|
>
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
{isConverting ? "Converting..." : "Convert & Download"}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
src/components/theme-provider.tsx
Normal file
9
src/components/theme-provider.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||||
|
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||||
|
|
||||||
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user