Skip to content

Commit

Permalink
feat: add Image AI Generator, Logo improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
akicool committed Jan 25, 2025
1 parent c4e5228 commit 672b6c1
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 11 deletions.
6 changes: 5 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import type { NextConfig } from "next";

const nextConfig: NextConfig = {
images: {
domains: ["localhost", "bnolkxefwhspjivjsugj.supabase.co"],
domains: [
"localhost",
"bnolkxefwhspjivjsugj.supabase.co",
"image.pollinations.ai",
],
},
};

Expand Down
22 changes: 21 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"react-dropzone": "^14.3.5",
"react-paginate": "^8.2.0",
"react-tiny-popover": "^8.1.4",
"react-toastify": "^11.0.2"
"react-toastify": "^11.0.2",
"spoiled": "^0.3.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
Expand Down
15 changes: 15 additions & 0 deletions src/app/generator/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import ImageGenerator from "@/components/Image/Generator";
import { Logo } from "@/shared/Logo";

export default async function GeneratorPage() {
return (
<main className="container mx-auto flex min-h-screen flex-col items-center justify-center px-10 py-5 sm:p-20 text-black relative gap-4">
<div className="absolute top-8 left-4">
<Logo />
</div>

<h1 className="text-4xl font-bold">Image AI</h1>
<ImageGenerator />
</main>
);
}
20 changes: 12 additions & 8 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { ImageUploader } from "../components/Image/Uploader";
import { ImageGallery } from "../components/Image/Gallery";
import { Rubik } from "next/font/google";
import clsx from "clsx";
import Link from "next/link";
import { Logo } from "@/shared/Logo";

type Props = {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};

const rubik = Rubik({
variable: "--font-rubik",
subsets: ["latin"],
});

export default async function Home({ searchParams }: Props) {
const { page = "1" } = await searchParams;

return (
<main className="container mx-auto py-8 px-4 h-dvh flex flex-col justify-between text-black">
<h1 className={clsx("text-3xl mb-4", rubik.variable)}>Image Hoster</h1>
<div className="flex items-center w-full justify-between mb-4">
<Logo />

<Link
href={"/generator"}
className="px-4 py-2 rounded-md bg-blue-500 font-semibold hover:bg-blue-600 transition duration-300 ease-in-out text-white"
>
Image AI
</Link>
</div>

<div className="mb-4 bg-white p-5 rounded-xl">
<h2 className="text-lg font-semibold mb-4">Загрузить изображение</h2>
Expand Down
175 changes: 175 additions & 0 deletions src/components/Image/Generator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"use client";

import clsx from "clsx";
import Image from "next/image";
import { ChangeEvent, useState, useRef } from "react";

export default function ImageGenerator() {
const [prompt, setPrompt] = useState("");
const [imageUrl, setImageUrl] = useState<string>("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const canvasRef = useRef<HTMLCanvasElement>(null);

// eslint-disable-next-line
const addWatermark = (originalImageUrl: string): Promise<string> => {
return new Promise((resolve, reject) => {
const image = new window.Image() as HTMLImageElement;
image.crossOrigin = "anonymous";
image.src = originalImageUrl;

image.onload = () => {
const canvas = canvasRef.current;
if (!canvas) return;

const ctx = canvas.getContext("2d");
if (!ctx) return;

canvas.width = image.width;
canvas.height = image.height;

ctx.drawImage(image, 0, 0);

ctx.fillStyle = "rgba(0, 0, 0)";
ctx.font = "bold 26px monospace";
ctx.lineWidth = 4;
ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";

const badgeText = "imagehoster.ru";

const textMetrics = ctx.measureText(badgeText);
const padding = 20;

const badgeX = canvas.width - textMetrics.width - padding * 2;
const badgeY = canvas.height - 45 - padding;
ctx.strokeRect(
badgeX,
badgeY,
textMetrics.width + padding * 2,
45 + padding
);

ctx.fillRect(
badgeX,
badgeY,
textMetrics.width + padding * 2,
45 + padding
);

ctx.fillStyle = "white";
ctx.fillText(
badgeText,
canvas.width - textMetrics.width - padding,
canvas.height - padding
);

const watermarkedImageUrl = canvas.toDataURL("image/png");
resolve(watermarkedImageUrl);
};

image.onerror = () => {
reject(new Error("Failed to load image"));
};
});
};

const generateImage = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError("");
setImageUrl("");

const uniqueId = String(Date.now()).slice(7, String(Date.now()).length);
const ModelAI = "flux-pro";

try {
const url = `https://image.pollinations.ai/prompt/${prompt}?seed=${uniqueId}&nologo=true&model=${ModelAI}&enhance=true`;

const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
};

const response = await fetch(url, options);
console.log("response:", response);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

// const watermarkedImage = await addWatermark(response.url);
setImageUrl(response.url);
} catch (error) {
console.error("Error generating image:", error);
setError(
error instanceof Error ? error.message : "An unknown error occurred"
);
} finally {
setIsLoading(false);
}
};

return (
<div className="w-full max-w-md flex flex-col gap-4">
{/* <canvas ref={canvasRef} style={{ display: "none" }} /> */}

{isLoading && <div className="file-loader !w-full" />}

<div
className={clsx(
"transition duration-1000 ease-in-out overflow-hidden",
imageUrl && !isLoading
? "max-h-full opacity-100"
: "max-h-0 opacity-0"
)}
style={{
maxHeight: imageUrl && !isLoading ? "500px" : "0px",
opacity: imageUrl && !isLoading ? 1 : 0,
transition:
imageUrl && !isLoading
? "max-height 500ms ease-out, opacity 500ms ease-out"
: "max-height 300ms ease-in, opacity 630ms ease-in",
}}
>
{imageUrl && (
<Image
src={imageUrl}
alt="Generated image"
className="w-full rounded-lg shadow-lg"
width={500}
height={500}
/>
)}
</div>

<form onSubmit={generateImage} className="space-y-4 text-black">
<textarea
value={prompt}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) =>
setPrompt(e.target.value)
}
rows={5}
placeholder="Введите описание изображения"
required
className="w-full p-2 border border-gray-300 rounded-lg h-fit text-wrap resize-none"
/>

<button
type="submit"
disabled={isLoading || !prompt}
className="w-full p-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400"
>
{isLoading ? "Генерирую..." : "Сгенерировать изображение"}
</button>
</form>

{error && (
<div className="mt-4 p-2 bg-red-100 border border-red-400 text-red-700 rounded">
{error}
</div>
)}
</div>
);
}
31 changes: 31 additions & 0 deletions src/shared/Logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { Rubik } from "next/font/google";
import clsx from "clsx";

import favicon from "../app/favicon.ico";

const rubik = Rubik({
variable: "--font-rubik",
subsets: ["latin"],
});

export const Logo = () => {
return (
<Link
href="/"
className="flex items-center gap-2 group lg:hover:text-blue-500 transition lg:duration-300 duration-200 ease-in-out active:text-blue-500"
>
<Image
src={favicon}
alt="logo"
width={30}
height={30}
className="rounded-lg lg:group-hover:rotate-12 transition lg:duration-300 duration-200 ease-in-out group-active:rotate-12"
/>

<h1 className={clsx("text-2xl", rubik.variable)}>Image Hoster</h1>
</Link>
);
};
1 change: 1 addition & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/shared/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
Expand Down

0 comments on commit 672b6c1

Please sign in to comment.