Upload 22 files
Browse files- app/_actions/generate.ts +88 -0
- app/_actions/logos.ts +39 -0
- app/_components/gallery/index.tsx +36 -0
- app/_components/gallery/list.tsx +54 -0
- app/_components/generation/index.tsx +79 -0
- app/_components/generation/step/brand.tsx +25 -0
- app/_components/generation/step/description.tsx +25 -0
- app/_components/generation/step/industry.tsx +32 -0
- app/_components/generation/step/list.tsx +74 -0
- app/_components/hero-header.tsx +71 -0
- app/_fonts/GeistMonoVF.woff +0 -0
- app/_fonts/GeistVF.woff +0 -0
- app/_fonts/nohemi/bold.woff +0 -0
- app/_fonts/nohemi/extrabold.woff +0 -0
- app/_fonts/nohemi/light.woff +0 -0
- app/_fonts/nohemi/regular.woff +0 -0
- app/_fonts/nohemi/semibold.woff +0 -0
- app/api/images/[id]/route.ts +33 -0
- app/favicon.ico +0 -0
- app/gallery/page.tsx +19 -0
- app/layout.tsx +87 -0
- app/page.tsx +23 -0
app/_actions/generate.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use server";
|
| 2 |
+
|
| 3 |
+
import { HfInference } from "@huggingface/inference";
|
| 4 |
+
import fs from "fs/promises";
|
| 5 |
+
|
| 6 |
+
import { Form } from "@/_types";
|
| 7 |
+
import prisma from "@/_utils/prisma";
|
| 8 |
+
|
| 9 |
+
export async function generate({ brand_name, industry, description }: Form) {
|
| 10 |
+
if (!process.env.PUBLIC_FILE_UPLOAD_DIR) {
|
| 11 |
+
throw new Error("PUBLIC_FILE_UPLOAD_DIR is not set");
|
| 12 |
+
}
|
| 13 |
+
const inference = new HfInference(process.env.HF_ACCESS_TOKEN, {
|
| 14 |
+
use_cache: false,
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
const prompt: any = await inference
|
| 18 |
+
.chatCompletion({
|
| 19 |
+
model: "meta-llama/Meta-Llama-3.1-70B-Instruct",
|
| 20 |
+
messages: [
|
| 21 |
+
{ role: "user", content: "lee, a noodle restaurant" },
|
| 22 |
+
{
|
| 23 |
+
role: "assistant",
|
| 24 |
+
content:
|
| 25 |
+
'logo,Minimalist,A pair of chopsticks and a bowl of rice with the word "Lee",',
|
| 26 |
+
},
|
| 27 |
+
{ role: "user", content: "cat shop" },
|
| 28 |
+
{ role: "assistant", content: "wablogo,Minimalist,Leaf and cat,logo," },
|
| 29 |
+
{ role: "user", content: "Ato, real estate company" },
|
| 30 |
+
{
|
| 31 |
+
role: "assistant",
|
| 32 |
+
content:
|
| 33 |
+
'logo,Minimalist,A man stands in front of a door,his shadow forming the word "A",',
|
| 34 |
+
},
|
| 35 |
+
{ role: "user", content: `${brand_name}, ${description}, ${industry}` },
|
| 36 |
+
],
|
| 37 |
+
temperature: 0.5,
|
| 38 |
+
max_tokens: 1024,
|
| 39 |
+
top_p: 0.7,
|
| 40 |
+
})
|
| 41 |
+
.then((res) => res)
|
| 42 |
+
.catch((err) => {
|
| 43 |
+
return { error: err.message };
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
if (prompt?.error) {
|
| 47 |
+
return {
|
| 48 |
+
error: prompt.error,
|
| 49 |
+
};
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
if (prompt?.choices[0]?.message?.content) {
|
| 53 |
+
const hfRequest = await inference.textToImage({
|
| 54 |
+
inputs: prompt.choices[0].message.content,
|
| 55 |
+
model: "Shakker-Labs/FLUX.1-dev-LoRA-Logo-Design",
|
| 56 |
+
parameters: {
|
| 57 |
+
num_inference_steps: 24,
|
| 58 |
+
guidance_scale: 3.5,
|
| 59 |
+
},
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
const buffer = await hfRequest.arrayBuffer();
|
| 63 |
+
const array = new Uint8Array(buffer);
|
| 64 |
+
|
| 65 |
+
const newImage = await prisma.logo.create({
|
| 66 |
+
data: {
|
| 67 |
+
name: prompt.choices[0].message.content,
|
| 68 |
+
},
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
const indexFile = newImage.id;
|
| 72 |
+
|
| 73 |
+
const dir = await fs
|
| 74 |
+
.opendir(process.env.PUBLIC_FILE_UPLOAD_DIR)
|
| 75 |
+
.catch(() => null);
|
| 76 |
+
if (!dir) await fs.mkdir(process.env.PUBLIC_FILE_UPLOAD_DIR);
|
| 77 |
+
await fs.writeFile(
|
| 78 |
+
`${process.env.PUBLIC_FILE_UPLOAD_DIR}/${indexFile}.png`,
|
| 79 |
+
array
|
| 80 |
+
);
|
| 81 |
+
|
| 82 |
+
return { data: indexFile };
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
return {
|
| 86 |
+
error: "Failed to generate logo",
|
| 87 |
+
};
|
| 88 |
+
}
|
app/_actions/logos.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use server";
|
| 2 |
+
|
| 3 |
+
import prisma from "@/_utils/prisma";
|
| 4 |
+
|
| 5 |
+
export const getLastLogos = async () => {
|
| 6 |
+
const images = await prisma.logo.findMany({
|
| 7 |
+
select: {
|
| 8 |
+
id: true,
|
| 9 |
+
},
|
| 10 |
+
take: 24,
|
| 11 |
+
orderBy: {
|
| 12 |
+
id: "desc",
|
| 13 |
+
},
|
| 14 |
+
});
|
| 15 |
+
return images.map((image) => image.id);
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
const ITEMS_PER_PAGE = 24;
|
| 19 |
+
|
| 20 |
+
export const getLogos = async (page: number = 0) => {
|
| 21 |
+
const images = await prisma.logo.findMany({
|
| 22 |
+
select: {
|
| 23 |
+
id: true,
|
| 24 |
+
},
|
| 25 |
+
skip: page * ITEMS_PER_PAGE,
|
| 26 |
+
take: ITEMS_PER_PAGE,
|
| 27 |
+
orderBy: {
|
| 28 |
+
id: "desc",
|
| 29 |
+
},
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
const total = await prisma.logo.count();
|
| 33 |
+
const hasMore = total > (page + 1) * ITEMS_PER_PAGE;
|
| 34 |
+
|
| 35 |
+
return {
|
| 36 |
+
logos: images.map((image) => image.id),
|
| 37 |
+
hasMore,
|
| 38 |
+
};
|
| 39 |
+
};
|
app/_components/gallery/index.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Image from "next/image";
|
| 2 |
+
import Link from "next/link";
|
| 3 |
+
|
| 4 |
+
export const Gallery = ({ logos }: { logos: Array<number> }) => {
|
| 5 |
+
return (
|
| 6 |
+
<section id="gallery" className="w-full py-10 lg:py-16">
|
| 7 |
+
<div className="mx-auto bg-amber-500/10 border border-amber-500/15 text-amber-500 px-3 py-1.5 rounded-full flex items-center gap-1 justify-center max-w-max text-xs mb-4">
|
| 8 |
+
<span className="text-xs">⚡</span>
|
| 9 |
+
Increase your creativity
|
| 10 |
+
</div>
|
| 11 |
+
<h3 className="max-w-4xl text-2xl lg:text-3xl text-[#aaaaaa] font-semibold mb-12 text-center mx-auto">
|
| 12 |
+
See our <span className="text-white">last designs</span>.
|
| 13 |
+
</h3>
|
| 14 |
+
<div className="max-lg:grid max-lg:grid-cols-2 lg:flex lg:items-start lg:justify-center gap-6 flex-wrap">
|
| 15 |
+
{logos.map((index) => (
|
| 16 |
+
<Image
|
| 17 |
+
key={index}
|
| 18 |
+
src={`/api/images/${index}`}
|
| 19 |
+
alt="Generated logo"
|
| 20 |
+
width={500}
|
| 21 |
+
height={500}
|
| 22 |
+
className="rounded-2xl w-full lg:size-72 object-cover"
|
| 23 |
+
/>
|
| 24 |
+
))}
|
| 25 |
+
</div>
|
| 26 |
+
<div className="mt-12 flex items-center justify-center">
|
| 27 |
+
<Link
|
| 28 |
+
href="/gallery"
|
| 29 |
+
className="rounded-full text-zinc-300 bg-zinc-900 font-medium text-base px-6 py-3 hover:bg-opacity-80 transition-all duration-150 text-center max-lg:w-full"
|
| 30 |
+
>
|
| 31 |
+
View all examples
|
| 32 |
+
</Link>
|
| 33 |
+
</div>
|
| 34 |
+
</section>
|
| 35 |
+
);
|
| 36 |
+
};
|
app/_components/gallery/list.tsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import InfiniteScroll from "react-infinite-scroll-component";
|
| 4 |
+
import Image from "next/image";
|
| 5 |
+
import { useState } from "react";
|
| 6 |
+
import { getLogos } from "@/app/_actions/logos";
|
| 7 |
+
|
| 8 |
+
export const InfiniteGallery = ({
|
| 9 |
+
logos: initialLogos,
|
| 10 |
+
hasMore: initialHasMore,
|
| 11 |
+
}: {
|
| 12 |
+
logos: Array<number>;
|
| 13 |
+
hasMore: boolean;
|
| 14 |
+
}) => {
|
| 15 |
+
const [page, setPage] = useState(0);
|
| 16 |
+
const [logos, setLogos] = useState([...initialLogos]);
|
| 17 |
+
const [hasMore, setHasMore] = useState(initialHasMore);
|
| 18 |
+
|
| 19 |
+
const fetchMoreData = async () => {
|
| 20 |
+
const newLogos = await getLogos(page + 1);
|
| 21 |
+
setLogos([...logos, ...newLogos.logos]);
|
| 22 |
+
setHasMore(newLogos.hasMore);
|
| 23 |
+
setPage(page + 1);
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<InfiniteScroll
|
| 28 |
+
scrollableTarget="content-wrapper"
|
| 29 |
+
dataLength={logos.length} //This is important field to render the next data
|
| 30 |
+
next={fetchMoreData}
|
| 31 |
+
hasMore={hasMore}
|
| 32 |
+
loader={
|
| 33 |
+
<div className="w-full max-lg:col-span-2 text-center">Loading...</div>
|
| 34 |
+
}
|
| 35 |
+
className="max-lg:grid max-lg:grid-cols-2 lg:flex lg:items-start lg:justify-center gap-6 flex-wrap"
|
| 36 |
+
endMessage={
|
| 37 |
+
<div className="w-full max-lg:col-span-2 text-zinc-400 text-center">
|
| 38 |
+
Yay! You have seen it all
|
| 39 |
+
</div>
|
| 40 |
+
}
|
| 41 |
+
>
|
| 42 |
+
{logos.map((index) => (
|
| 43 |
+
<Image
|
| 44 |
+
key={index}
|
| 45 |
+
src={`/api/images/${index}`}
|
| 46 |
+
alt="Generated logo"
|
| 47 |
+
width={500}
|
| 48 |
+
height={500}
|
| 49 |
+
className="rounded-2xl w-full lg:size-72 object-cover"
|
| 50 |
+
/>
|
| 51 |
+
))}
|
| 52 |
+
</InfiniteScroll>
|
| 53 |
+
);
|
| 54 |
+
};
|
app/_components/generation/index.tsx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState } from "react";
|
| 4 |
+
|
| 5 |
+
import { Form } from "@/_types";
|
| 6 |
+
import { generate } from "@/app/_actions/generate";
|
| 7 |
+
|
| 8 |
+
import { Brand } from "./step/brand";
|
| 9 |
+
import { Steps } from "./step/list";
|
| 10 |
+
import { Industry } from "./step/industry";
|
| 11 |
+
import { Description } from "./step/description";
|
| 12 |
+
import classNames from "classnames";
|
| 13 |
+
import { toast } from "react-toastify";
|
| 14 |
+
import Image from "next/image";
|
| 15 |
+
|
| 16 |
+
export const Generation = () => {
|
| 17 |
+
const [form, setForm] = useState<Form>({
|
| 18 |
+
brand_name: "",
|
| 19 |
+
display_name: false,
|
| 20 |
+
description: "",
|
| 21 |
+
industry: "",
|
| 22 |
+
style: "",
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
const [loading, setLoading] = useState<boolean>(false);
|
| 26 |
+
const [result, setResult] = useState<number | undefined>(undefined);
|
| 27 |
+
|
| 28 |
+
const handleGenerate = async () => {
|
| 29 |
+
if (loading) return;
|
| 30 |
+
setLoading(true);
|
| 31 |
+
try {
|
| 32 |
+
const response = await generate(form);
|
| 33 |
+
setResult(response.data);
|
| 34 |
+
} catch (err) {
|
| 35 |
+
toast.error("An error occurred. Please try again later.");
|
| 36 |
+
} finally {
|
| 37 |
+
setLoading(false);
|
| 38 |
+
}
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
return (
|
| 42 |
+
<main id="generation" className="w-full py-10 lg:py-20">
|
| 43 |
+
<h3 className="max-w-4xl text-2xl lg:text-3xl text-[#aaaaaa] font-semibold mb-12 text-center mx-auto">
|
| 44 |
+
Start your <span className="text-white">generation</span> here.
|
| 45 |
+
</h3>
|
| 46 |
+
<Steps form={form} />
|
| 47 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 lg:gap-20">
|
| 48 |
+
<Brand form={form} setForm={setForm} />
|
| 49 |
+
<Description form={form} setForm={setForm} />
|
| 50 |
+
<Industry form={form} setForm={setForm} />
|
| 51 |
+
<div className="lg:col-span-3 flex items-center justify-center">
|
| 52 |
+
<button
|
| 53 |
+
className={classNames(
|
| 54 |
+
"max-lg:w-full rounded-full bg-white text-zinc-950 font-medium text-sm px-6 py-3 hover:bg-opacity-80 transition-all duration-150 disabled:bg-zinc-500 disabled:text-zinc-700",
|
| 55 |
+
{
|
| 56 |
+
"animate-pulse": loading,
|
| 57 |
+
}
|
| 58 |
+
)}
|
| 59 |
+
disabled={!form.brand_name || !form.description || !form.industry}
|
| 60 |
+
onClick={handleGenerate}
|
| 61 |
+
>
|
| 62 |
+
{loading ? "Generating..." : "Generate my Logo"}
|
| 63 |
+
</button>
|
| 64 |
+
</div>
|
| 65 |
+
{result && (
|
| 66 |
+
<div className="lg:col-span-3 flex items-center justify-center rounded-3xl">
|
| 67 |
+
<Image
|
| 68 |
+
src={`/api/images/${result}`}
|
| 69 |
+
alt="Generated logo"
|
| 70 |
+
className="h-[300px]"
|
| 71 |
+
width={400}
|
| 72 |
+
height={400}
|
| 73 |
+
/>
|
| 74 |
+
</div>
|
| 75 |
+
)}
|
| 76 |
+
</div>
|
| 77 |
+
</main>
|
| 78 |
+
);
|
| 79 |
+
};
|
app/_components/generation/step/brand.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Form } from "@/_types";
|
| 2 |
+
|
| 3 |
+
export const Brand = ({
|
| 4 |
+
form,
|
| 5 |
+
setForm,
|
| 6 |
+
}: {
|
| 7 |
+
form: Form;
|
| 8 |
+
setForm: React.Dispatch<React.SetStateAction<Form>>;
|
| 9 |
+
}) => {
|
| 10 |
+
return (
|
| 11 |
+
<div className="w-full">
|
| 12 |
+
<label htmlFor="brand_name" className="text-zinc-300 mb-1 block text-sm">
|
| 13 |
+
Brand name
|
| 14 |
+
</label>
|
| 15 |
+
<input
|
| 16 |
+
type="text"
|
| 17 |
+
id="brand_name"
|
| 18 |
+
placeholder="Hugging Face"
|
| 19 |
+
value={form.brand_name}
|
| 20 |
+
className="border bg-zinc-900 border-zinc-800 rounded-lg py-2 px-4 text-gray-200 outline-none w-full placeholder:text-gray-600"
|
| 21 |
+
onChange={(e) => setForm({ ...form, brand_name: e.target.value })}
|
| 22 |
+
/>
|
| 23 |
+
</div>
|
| 24 |
+
);
|
| 25 |
+
};
|
app/_components/generation/step/description.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Form } from "@/_types";
|
| 2 |
+
|
| 3 |
+
export const Description = ({
|
| 4 |
+
form,
|
| 5 |
+
setForm,
|
| 6 |
+
}: {
|
| 7 |
+
form: Form;
|
| 8 |
+
setForm: React.Dispatch<React.SetStateAction<Form>>;
|
| 9 |
+
}) => {
|
| 10 |
+
return (
|
| 11 |
+
<div className="w-full">
|
| 12 |
+
<label htmlFor="description" className="text-zinc-300 mb-1 block text-sm">
|
| 13 |
+
Short Description
|
| 14 |
+
</label>
|
| 15 |
+
<input
|
| 16 |
+
type="text"
|
| 17 |
+
id="description"
|
| 18 |
+
placeholder="A platform for building and sharing models"
|
| 19 |
+
value={form.description}
|
| 20 |
+
className="border bg-zinc-900 border-zinc-800 rounded-lg py-2 px-4 text-gray-200 outline-none w-full placeholder:text-gray-600"
|
| 21 |
+
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
| 22 |
+
/>
|
| 23 |
+
</div>
|
| 24 |
+
);
|
| 25 |
+
};
|
app/_components/generation/step/industry.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Form } from "@/_types";
|
| 2 |
+
|
| 3 |
+
import { INDUSTRIES } from "@/_utils";
|
| 4 |
+
|
| 5 |
+
export const Industry = ({
|
| 6 |
+
form,
|
| 7 |
+
setForm,
|
| 8 |
+
}: {
|
| 9 |
+
form: Form;
|
| 10 |
+
setForm: React.Dispatch<React.SetStateAction<Form>>;
|
| 11 |
+
}) => {
|
| 12 |
+
return (
|
| 13 |
+
<div className="">
|
| 14 |
+
<label htmlFor="industry" className="text-zinc-300 mb-1 block text-sm">
|
| 15 |
+
Industry
|
| 16 |
+
</label>
|
| 17 |
+
<select
|
| 18 |
+
id="industry"
|
| 19 |
+
className="border bg-zinc-900 border-zinc-800 rounded-lg py-3 px-4 text-gray-200 outline-none w-full"
|
| 20 |
+
onChange={(e) => setForm({ ...form, industry: e.target.value })}
|
| 21 |
+
>
|
| 22 |
+
<option value="">Select an industry</option>
|
| 23 |
+
{INDUSTRIES.map((industry, i) => (
|
| 24 |
+
<option key={i} value={industry.name}>
|
| 25 |
+
{/* <industry.icon className="mr-2 inline-block" /> */}
|
| 26 |
+
{industry.name}
|
| 27 |
+
</option>
|
| 28 |
+
))}
|
| 29 |
+
</select>
|
| 30 |
+
</div>
|
| 31 |
+
);
|
| 32 |
+
};
|
app/_components/generation/step/list.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { TiLightbulb } from "react-icons/ti";
|
| 2 |
+
import { MdWorkOutline } from "react-icons/md";
|
| 3 |
+
import { IoMdCheckmark } from "react-icons/io";
|
| 4 |
+
import { MdOutlinePermIdentity } from "react-icons/md";
|
| 5 |
+
import classNames from "classnames";
|
| 6 |
+
|
| 7 |
+
import { Form } from "@/_types";
|
| 8 |
+
|
| 9 |
+
const STEPS = [
|
| 10 |
+
{
|
| 11 |
+
title: "Brand",
|
| 12 |
+
description: "Tell us about your brand.",
|
| 13 |
+
icon: MdOutlinePermIdentity,
|
| 14 |
+
active: "bg-amber-500 !border-amber-500",
|
| 15 |
+
key: "brand_name",
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
title: "Concept",
|
| 19 |
+
description: "What's your brand about?",
|
| 20 |
+
icon: TiLightbulb,
|
| 21 |
+
active: "bg-violet-500 !border-violet-500",
|
| 22 |
+
key: "description",
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
title: "Industry",
|
| 26 |
+
description: "What industry are you in?",
|
| 27 |
+
icon: MdWorkOutline,
|
| 28 |
+
active: "bg-emerald-500 !border-emerald-500",
|
| 29 |
+
key: "industry",
|
| 30 |
+
},
|
| 31 |
+
];
|
| 32 |
+
|
| 33 |
+
export const Steps = ({ form }: { form: Form }) => {
|
| 34 |
+
return (
|
| 35 |
+
<div className="max-lg:hidden w-full flex items-center justify-center gap-2 mb-12">
|
| 36 |
+
{STEPS.map((s, i) => (
|
| 37 |
+
<>
|
| 38 |
+
<div
|
| 39 |
+
key={i}
|
| 40 |
+
className={classNames(
|
| 41 |
+
"text-center flex flex-col items-center min-w-60 cursor-pointer",
|
| 42 |
+
{
|
| 43 |
+
"opacity-40": form[s.key as keyof typeof form] === "",
|
| 44 |
+
}
|
| 45 |
+
)}
|
| 46 |
+
>
|
| 47 |
+
<div
|
| 48 |
+
className={classNames(
|
| 49 |
+
"size-7 border border-white text-white flex items-center justify-center rounded-2xl mb-3",
|
| 50 |
+
{
|
| 51 |
+
[s.active]: form[s.key as keyof typeof form],
|
| 52 |
+
}
|
| 53 |
+
)}
|
| 54 |
+
>
|
| 55 |
+
{form[s.key as keyof typeof form] ? (
|
| 56 |
+
<IoMdCheckmark className="text-xl" />
|
| 57 |
+
) : (
|
| 58 |
+
<s.icon className="text-base" />
|
| 59 |
+
)}
|
| 60 |
+
</div>
|
| 61 |
+
<p className="text-white text-xl font-semibold">{s.title}</p>
|
| 62 |
+
<p className="text-white text-sm font-mono">{s.description}</p>
|
| 63 |
+
</div>
|
| 64 |
+
{i !== STEPS.length - 1 && (
|
| 65 |
+
<div
|
| 66 |
+
key={"linear_" + i}
|
| 67 |
+
className="h-0 flex-1 border-gray-100/20 border-dashed border-b"
|
| 68 |
+
/>
|
| 69 |
+
)}
|
| 70 |
+
</>
|
| 71 |
+
))}
|
| 72 |
+
</div>
|
| 73 |
+
);
|
| 74 |
+
};
|
app/_components/hero-header.tsx
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import classNames from "classnames";
|
| 4 |
+
import { useState } from "react";
|
| 5 |
+
import { useInterval } from "react-use";
|
| 6 |
+
|
| 7 |
+
export const HeroHeader = () => {
|
| 8 |
+
const [selectedWord, setSelectedWord] = useState("Think.");
|
| 9 |
+
|
| 10 |
+
useInterval(() => {
|
| 11 |
+
setSelectedWord((prev) => {
|
| 12 |
+
switch (prev) {
|
| 13 |
+
case "Think.":
|
| 14 |
+
return "Customize.";
|
| 15 |
+
case "Customize.":
|
| 16 |
+
return "Generate.";
|
| 17 |
+
case "Generate.":
|
| 18 |
+
return "Think.";
|
| 19 |
+
default:
|
| 20 |
+
return "Think.";
|
| 21 |
+
}
|
| 22 |
+
});
|
| 23 |
+
}, 2000);
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<header className="py-14 lg:py-20 grid grid-cols-1 gap-5">
|
| 27 |
+
<h1 className="text-4xl lg:text-5xl font-semibold text-[#aaaaaa] max-w-max mx-auto text-center">
|
| 28 |
+
<span
|
| 29 |
+
className={classNames("transition-all duration-300 text-opacity-0", {
|
| 30 |
+
"text-white !text-opacity-100": selectedWord === "Think.",
|
| 31 |
+
})}
|
| 32 |
+
>
|
| 33 |
+
Think.
|
| 34 |
+
</span>{" "}
|
| 35 |
+
<span
|
| 36 |
+
className={classNames("transition-all duration-300 text-opacity-0", {
|
| 37 |
+
"text-white !text-opacity-100": selectedWord === "Customize.",
|
| 38 |
+
})}
|
| 39 |
+
>
|
| 40 |
+
Customize.
|
| 41 |
+
</span>{" "}
|
| 42 |
+
<br />
|
| 43 |
+
and{" "}
|
| 44 |
+
<span
|
| 45 |
+
className={classNames("transition-all duration-300 text-opacity-0", {
|
| 46 |
+
"text-white !text-opacity-100": selectedWord === "Generate.",
|
| 47 |
+
})}
|
| 48 |
+
>
|
| 49 |
+
Generate.
|
| 50 |
+
</span>
|
| 51 |
+
</h1>
|
| 52 |
+
<h2 className="text-lg font-light text-center text-[#898989] max-w-sm mx-auto">
|
| 53 |
+
An AI powered tool that helps you create a logo for your brand.
|
| 54 |
+
</h2>
|
| 55 |
+
<div className="max-lg:max-w-xs w-full max-lg:mx-auto max-lg:grid lg:flex lg:items-center lg:justify-center gap-6 mt-3 ">
|
| 56 |
+
<a
|
| 57 |
+
href="#generation"
|
| 58 |
+
className="rounded-full bg-white text-zinc-950 font-medium text-base px-6 py-3 hover:bg-opacity-80 transition-all duration-150 text-center max-lg:w-full"
|
| 59 |
+
>
|
| 60 |
+
Start generation
|
| 61 |
+
</a>
|
| 62 |
+
<a
|
| 63 |
+
href="#gallery"
|
| 64 |
+
className="rounded-full text-zinc-300 bg-zinc-900 font-medium text-base px-6 py-3 hover:bg-opacity-80 transition-all duration-150 text-center max-lg:w-full"
|
| 65 |
+
>
|
| 66 |
+
View examples
|
| 67 |
+
</a>
|
| 68 |
+
</div>
|
| 69 |
+
</header>
|
| 70 |
+
);
|
| 71 |
+
};
|
app/_fonts/GeistMonoVF.woff
ADDED
|
Binary file (67.9 kB). View file
|
|
|
app/_fonts/GeistVF.woff
ADDED
|
Binary file (66.3 kB). View file
|
|
|
app/_fonts/nohemi/bold.woff
ADDED
|
Binary file (24.9 kB). View file
|
|
|
app/_fonts/nohemi/extrabold.woff
ADDED
|
Binary file (24.7 kB). View file
|
|
|
app/_fonts/nohemi/light.woff
ADDED
|
Binary file (32.9 kB). View file
|
|
|
app/_fonts/nohemi/regular.woff
ADDED
|
Binary file (25.6 kB). View file
|
|
|
app/_fonts/nohemi/semibold.woff
ADDED
|
Binary file (25.4 kB). View file
|
|
|
app/api/images/[id]/route.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest } from "next/server";
|
| 2 |
+
import fs from "fs/promises";
|
| 3 |
+
|
| 4 |
+
import prisma from "@/_utils/prisma";
|
| 5 |
+
|
| 6 |
+
export async function GET(
|
| 7 |
+
req: NextRequest,
|
| 8 |
+
{ params }: { params: { id: string } }
|
| 9 |
+
) {
|
| 10 |
+
const { id } = params;
|
| 11 |
+
|
| 12 |
+
const image = await prisma.logo.findUnique({
|
| 13 |
+
where: { id: Number(id) },
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
if (!image) {
|
| 17 |
+
return new Response(null, { status: 404 });
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const buffer = await fs.readFile(
|
| 21 |
+
`${process.env.PUBLIC_FILE_UPLOAD_DIR}/${image.id}.png`
|
| 22 |
+
);
|
| 23 |
+
|
| 24 |
+
if (!buffer) {
|
| 25 |
+
return new Response(null, { status: 404 });
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
return new Response(buffer, {
|
| 29 |
+
headers: {
|
| 30 |
+
"Content-Type": "image/png",
|
| 31 |
+
},
|
| 32 |
+
});
|
| 33 |
+
}
|
app/favicon.ico
ADDED
|
|
app/gallery/page.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { InfiniteGallery } from "@/app/_components/gallery/list";
|
| 2 |
+
import { getLogos } from "@/app/_actions/logos";
|
| 3 |
+
|
| 4 |
+
async function lastLogos() {
|
| 5 |
+
const logos = await getLogos();
|
| 6 |
+
return logos;
|
| 7 |
+
}
|
| 8 |
+
export const revalidate = 0;
|
| 9 |
+
|
| 10 |
+
export default async function Gallery() {
|
| 11 |
+
const { hasMore, logos } = await lastLogos();
|
| 12 |
+
return (
|
| 13 |
+
<section className="w-full py-10 lg:py-16">
|
| 14 |
+
<div className="max-lg:grid max-lg:grid-cols-2 lg:flex lg:items-start lg:justify-center gap-6 flex-wrap">
|
| 15 |
+
<InfiniteGallery logos={logos} hasMore={hasMore} />
|
| 16 |
+
</div>
|
| 17 |
+
</section>
|
| 18 |
+
);
|
| 19 |
+
}
|
app/layout.tsx
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import localFont from "next/font/local";
|
| 3 |
+
import { ToastContainer } from "react-toastify";
|
| 4 |
+
import "react-toastify/dist/ReactToastify.css";
|
| 5 |
+
|
| 6 |
+
import "@/assets/globals.css";
|
| 7 |
+
import { Navigation } from "@/components/_navigation";
|
| 8 |
+
|
| 9 |
+
const nohemiRegular = localFont({
|
| 10 |
+
src: [
|
| 11 |
+
{
|
| 12 |
+
path: "./_fonts/nohemi/light.woff",
|
| 13 |
+
weight: "300",
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
path: "./_fonts/nohemi/regular.woff",
|
| 17 |
+
weight: "400",
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
path: "./_fonts/nohemi/semibold.woff",
|
| 21 |
+
weight: "600",
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
path: "./_fonts/nohemi/bold.woff",
|
| 25 |
+
weight: "700",
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
path: "./_fonts/nohemi/extrabold.woff",
|
| 29 |
+
weight: "900",
|
| 30 |
+
},
|
| 31 |
+
],
|
| 32 |
+
variable: "--font-nohemi-sans",
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
const geistMono = localFont({
|
| 36 |
+
src: "./_fonts/GeistMonoVF.woff",
|
| 37 |
+
variable: "--font-geist-mono",
|
| 38 |
+
weight: "100 900",
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
export const metadata: Metadata = {
|
| 42 |
+
title: "Create Next App",
|
| 43 |
+
description: "Generated by create next app",
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
export default function RootLayout({
|
| 47 |
+
children,
|
| 48 |
+
}: Readonly<{
|
| 49 |
+
children: React.ReactNode;
|
| 50 |
+
}>) {
|
| 51 |
+
return (
|
| 52 |
+
<html lang="en">
|
| 53 |
+
<body
|
| 54 |
+
className={`${nohemiRegular.variable} ${geistMono.variable} antialiased`}
|
| 55 |
+
>
|
| 56 |
+
<div
|
| 57 |
+
id="content-wrapper"
|
| 58 |
+
className="h-screen w-full overflow-auto font-[family-name:var(--font-nohemi-sans)] p-6 scroll-smooth"
|
| 59 |
+
>
|
| 60 |
+
<Navigation />
|
| 61 |
+
{children}
|
| 62 |
+
<footer className="mt-4 w-full max-w-4xl mx-auto border-t border-zinc-800 pt-8 pb-3 text-center">
|
| 63 |
+
<p className="text-sm text-zinc-400">
|
| 64 |
+
Powered by{" "}
|
| 65 |
+
<a
|
| 66 |
+
href="https://github.com/huggingface/huggingface.js"
|
| 67 |
+
target="_blank"
|
| 68 |
+
className="font-mono text-amber-500 hover:text-amber-400"
|
| 69 |
+
>
|
| 70 |
+
huggingface.js
|
| 71 |
+
</a>{" "}
|
| 72 |
+
and{" "}
|
| 73 |
+
<a
|
| 74 |
+
href="https://huggingface.co/Shakker-Labs/FLUX.1-dev-LoRA-Logo-Design"
|
| 75 |
+
target="_blank"
|
| 76 |
+
className="font-mono text-zinc-100 hover:text-white"
|
| 77 |
+
>
|
| 78 |
+
Shakker-Labs/FLUX.1-dev-LoRA-Logo-Design
|
| 79 |
+
</a>
|
| 80 |
+
</p>
|
| 81 |
+
</footer>
|
| 82 |
+
</div>
|
| 83 |
+
<ToastContainer />
|
| 84 |
+
</body>
|
| 85 |
+
</html>
|
| 86 |
+
);
|
| 87 |
+
}
|
app/page.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getLastLogos } from "./_actions/logos";
|
| 2 |
+
import { Gallery } from "./_components/gallery";
|
| 3 |
+
import { Generation } from "./_components/generation";
|
| 4 |
+
import { HeroHeader } from "./_components/hero-header";
|
| 5 |
+
|
| 6 |
+
async function lastLogos() {
|
| 7 |
+
const logos = await getLastLogos();
|
| 8 |
+
return logos;
|
| 9 |
+
}
|
| 10 |
+
export const revalidate = 0;
|
| 11 |
+
|
| 12 |
+
export default async function Home() {
|
| 13 |
+
const logos = await lastLogos();
|
| 14 |
+
return (
|
| 15 |
+
<section>
|
| 16 |
+
<div className="max-w-4xl mx-auto">
|
| 17 |
+
<HeroHeader />
|
| 18 |
+
<Generation />
|
| 19 |
+
</div>
|
| 20 |
+
<Gallery logos={logos} />
|
| 21 |
+
</section>
|
| 22 |
+
);
|
| 23 |
+
}
|