├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── app ├── api │ └── generate-logo │ │ └── route.ts ├── components │ ├── Footer.tsx │ ├── Header.tsx │ ├── InfoToolTip.tsx │ ├── Spinner.tsx │ └── ui │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ ├── select.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── tooltip.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── lib │ ├── domain.ts │ └── utils.ts └── page.tsx ├── components.json ├── hooks └── use-toast.ts ├── lib └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── Github.svg ├── abstract.svg ├── flashy.svg ├── generate-icon.svg ├── insan.png ├── minimal.svg ├── modern.svg ├── og-image.png ├── playful.svg ├── side.svg ├── solo.svg ├── stack.svg ├── tech.svg ├── together-ai-logo.svg ├── together-ai-logo1.svg └── twitter.svg ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | UPSTASH_REDIS_REST_URL= 2 | UPSTASH_REDIS_REST_TOKEN= 3 | TOGETHER_API_KEY= 4 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 5 | CLERK_SECRET_KEY= 6 | # optional 7 | HELICONE_API_KEY= 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | .env 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {"plugins": ["prettier-plugin-tailwindcss"]} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | AI Logo Generator 3 |

AI Logo Generator

4 |
5 | 6 |

7 | An open source logo generator – create professional logos in seconds with customizable styles. 8 |

9 | 10 | ## Tech stack 11 | 12 | - [Flux Pro 1.1](https://togetherai.link/flux-playground) on [Together AI](https://togetherai.link/) for logo generation 13 | - [Next.js](https://nextjs.org/) with TypeScript for the app framework 14 | - [Shadcn](https://ui.shadcn.com/) for UI components & [Tailwind](https://tailwindcss.com/) for styling 15 | - [Upstash Redis](https://upstash.com/) for rate limiting 16 | - [Clerk](https://clerk.com/) for authentication 17 | - [Plausible](https://plausible.io/) & [Helicone](https://helicone.ai/) for analytics & observability 18 | 19 | ## Cloning & running 20 | 21 | 1. Clone the repo: `git clone https://github.com/Nutlope/logocreator` 22 | 2. Create a `.env` file and add your [Together AI API key](https://api.together.xyz/settings/api-keys): `TOGETHER_API_KEY=` 23 | 3. Run `npm install` and `npm run dev` to install dependencies and run locally. 24 | 25 | ## Future Tasks 26 | 27 | - [ ] Create a dashboard with a user's logo history 28 | - [ ] Support SVG exports instead of just PNG 29 | - [ ] Add support for additional styles 30 | - [ ] Add a dropdown for image size (can do up to 1440x1440) 31 | - [ ] Show approximate price when using your own Together AI key 32 | - [ ] Allow the ability to upload a reference logo (use vision model to read it) 33 | - [ ] Redesign popular brand’s logos with my logo maker and have it in a showcase 34 | -------------------------------------------------------------------------------- /app/api/generate-logo/route.ts: -------------------------------------------------------------------------------- 1 | import { clerkClient, currentUser } from "@clerk/nextjs/server"; 2 | import { Ratelimit } from "@upstash/ratelimit"; 3 | import { Redis } from "@upstash/redis"; 4 | import dedent from "dedent"; 5 | import Together from "together-ai"; 6 | import { z } from "zod"; 7 | 8 | let ratelimit: Ratelimit | undefined; 9 | 10 | export async function POST(req: Request) { 11 | const user = await currentUser(); 12 | 13 | if (!user) { 14 | return new Response("", { status: 404 }); 15 | } 16 | 17 | const json = await req.json(); 18 | const data = z 19 | .object({ 20 | userAPIKey: z.string().optional(), 21 | companyName: z.string(), 22 | // selectedLayout: z.string(), 23 | selectedStyle: z.string(), 24 | selectedPrimaryColor: z.string(), 25 | selectedBackgroundColor: z.string(), 26 | additionalInfo: z.string().optional(), 27 | }) 28 | .parse(json); 29 | 30 | // Add observability if a Helicone key is specified, otherwise skip 31 | const options: ConstructorParameters[0] = {}; 32 | if (process.env.HELICONE_API_KEY) { 33 | options.baseURL = "https://together.helicone.ai/v1"; 34 | options.defaultHeaders = { 35 | "Helicone-Auth": `Bearer ${process.env.HELICONE_API_KEY}`, 36 | "Helicone-Property-LOGOBYOK": data.userAPIKey ? "true" : "false", 37 | }; 38 | } 39 | 40 | // Add rate limiting if Upstash API keys are set & no BYOK, otherwise skip 41 | if (process.env.UPSTASH_REDIS_REST_URL && !data.userAPIKey) { 42 | ratelimit = new Ratelimit({ 43 | redis: Redis.fromEnv(), 44 | // Allow 3 requests per 2 months on prod 45 | limiter: Ratelimit.fixedWindow(3, "60 d"), 46 | analytics: true, 47 | prefix: "logocreator", 48 | }); 49 | } 50 | 51 | const client = new Together(options); 52 | 53 | if (data.userAPIKey) { 54 | client.apiKey = data.userAPIKey; 55 | (await clerkClient()).users.updateUserMetadata(user.id, { 56 | unsafeMetadata: { 57 | remaining: "BYOK", 58 | }, 59 | }); 60 | } 61 | 62 | if (ratelimit) { 63 | const identifier = user.id; 64 | const { success, remaining } = await ratelimit.limit(identifier); 65 | (await clerkClient()).users.updateUserMetadata(user.id, { 66 | unsafeMetadata: { 67 | remaining, 68 | }, 69 | }); 70 | 71 | if (!success) { 72 | return new Response( 73 | "You've used up all your credits. Enter your own Together API Key to generate more logos.", 74 | { 75 | status: 429, 76 | headers: { "Content-Type": "text/plain" }, 77 | }, 78 | ); 79 | } 80 | } 81 | 82 | const flashyStyle = 83 | "Flashy, attention grabbing, bold, futuristic, and eye-catching. Use vibrant neon colors with metallic, shiny, and glossy accents."; 84 | 85 | const techStyle = 86 | "highly detailed, sharp focus, cinematic, photorealistic, Minimalist, clean, sleek, neutral color pallete with subtle accents, clean lines, shadows, and flat."; 87 | 88 | const modernStyle = 89 | "modern, forward-thinking, flat design, geometric shapes, clean lines, natural colors with subtle accents, use strategic negative space to create visual interest."; 90 | 91 | const playfulStyle = 92 | "playful, lighthearted, bright bold colors, rounded shapes, lively."; 93 | 94 | const abstractStyle = 95 | "abstract, artistic, creative, unique shapes, patterns, and textures to create a visually interesting and wild logo."; 96 | 97 | const minimalStyle = 98 | "minimal, simple, timeless, versatile, single color logo, use negative space, flat design with minimal details, Light, soft, and subtle."; 99 | 100 | const styleLookup: Record = { 101 | Flashy: flashyStyle, 102 | Tech: techStyle, 103 | Modern: modernStyle, 104 | Playful: playfulStyle, 105 | Abstract: abstractStyle, 106 | Minimal: minimalStyle, 107 | }; 108 | 109 | const prompt = dedent`A single logo, high-quality, award-winning professional design, made for both digital and print media, only contains a few vector shapes, ${styleLookup[data.selectedStyle]} 110 | 111 | Primary color is ${data.selectedPrimaryColor.toLowerCase()} and background color is ${data.selectedBackgroundColor.toLowerCase()}. The company name is ${data.companyName}, make sure to include the company name in the logo. ${data.additionalInfo ? `Additional info: ${data.additionalInfo}` : ""}`; 112 | 113 | try { 114 | const response = await client.images.create({ 115 | prompt, 116 | model: "black-forest-labs/FLUX.1.1-pro", 117 | width: 768, 118 | height: 768, 119 | // @ts-expect-error - this is not typed in the API 120 | response_format: "base64", 121 | }); 122 | return Response.json(response.data[0], { status: 200 }); 123 | } catch (error) { 124 | const invalidApiKey = z 125 | .object({ 126 | error: z.object({ 127 | error: z.object({ code: z.literal("invalid_api_key") }), 128 | }), 129 | }) 130 | .safeParse(error); 131 | 132 | if (invalidApiKey.success) { 133 | return new Response("Your API key is invalid.", { 134 | status: 401, 135 | headers: { "Content-Type": "text/plain" }, 136 | }); 137 | } 138 | 139 | const modelBlocked = z 140 | .object({ 141 | error: z.object({ 142 | error: z.object({ type: z.literal("request_blocked") }), 143 | }), 144 | }) 145 | .safeParse(error); 146 | 147 | if (modelBlocked.success) { 148 | return new Response( 149 | "Your Together AI account needs to be in Build Tier 2 ($50 credit pack purchase required) to use this model. Please make a purchase at: https://api.together.xyz/settings/billing", 150 | { 151 | status: 403, 152 | headers: { "Content-Type": "text/plain" }, 153 | }, 154 | ); 155 | } 156 | 157 | throw error; 158 | } 159 | } 160 | 161 | export const runtime = "edge"; 162 | -------------------------------------------------------------------------------- /app/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import TwitterSVG from "../../public/twitter.svg"; 2 | import GithubSVG from "../../public/Github.svg"; 3 | import Image from "next/image"; 4 | 5 | const Footer = () => ( 6 |
7 |
8 | 9 | Powered by{" "} 10 | 11 | Together.ai 12 | {" "} 13 | &{" "} 14 | 15 | Flux 16 | 17 | 18 |
19 |
20 | 24 | Twitter 25 | Github 26 | 27 | 31 | github 32 | Twitter 33 | 34 |
35 |
36 | ); 37 | 38 | export default Footer; 39 | -------------------------------------------------------------------------------- /app/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import { 4 | SignInButton, 5 | SignedIn, 6 | SignedOut, 7 | UserButton, 8 | useUser, 9 | } from "@clerk/nextjs"; 10 | import { domain } from "@/app/lib/domain"; 11 | 12 | export default function Header({ className }: { className: string }) { 13 | const { user } = useUser(); 14 | 15 | return ( 16 |
17 |
18 | {/* Logo - left on mobile, centered on larger screens */} 19 |
20 | 21 | together.ai 29 | 30 |
31 | {/* Credits Section */} 32 |
33 | 34 | 39 | 40 | 41 | {user?.unsafeMetadata.remaining === "BYOK" ? ( 42 |

Your API key

43 | ) : ( 44 |

Credits: {`${user?.unsafeMetadata.remaining ?? 3}`}

45 | )} 46 | 47 |
48 |
49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/components/InfoToolTip.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Tooltip, 3 | TooltipContent, 4 | TooltipProvider, 5 | TooltipTrigger, 6 | } from "@/components/ui/tooltip"; 7 | import { Info } from "lucide-react"; 8 | 9 | export default function InfoTooltip({ content }: { content: string }) { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 |

{content}

18 |
19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export default function Spinner({ 4 | loading = true, 5 | children, 6 | className = "", 7 | }: { 8 | loading?: boolean; 9 | children?: ReactNode; 10 | className?: string; 11 | }) { 12 | if (!loading) return children; 13 | 14 | const spinner = ( 15 | <> 16 | 28 | 29 | {Array.from(Array(8).keys()).map((i) => ( 30 | 38 | ))} 39 | 40 | 41 | ); 42 | 43 | if (!children) return spinner; 44 | 45 | return ( 46 | 47 | {children} 48 | 49 | 50 | {spinner} 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /app/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-white disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-12 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /app/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { X } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Dialog = DialogPrimitive.Root; 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger; 12 | 13 | const DialogPortal = DialogPrimitive.Portal; 14 | 15 | const DialogClose = DialogPrimitive.Close; 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )); 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )); 54 | DialogContent.displayName = DialogPrimitive.Content.displayName; 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ); 68 | DialogHeader.displayName = "DialogHeader"; 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ); 82 | DialogFooter.displayName = "DialogFooter"; 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )); 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | }; 123 | -------------------------------------------------------------------------------- /app/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = "Input"; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /app/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SelectPrimitive from "@radix-ui/react-select"; 5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Select = SelectPrimitive.Root; 10 | 11 | const SelectGroup = SelectPrimitive.Group; 12 | 13 | const SelectValue = SelectPrimitive.Value; 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | span]:line-clamp-1", 23 | className, 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | 29 | 30 | 31 | 32 | )); 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | 48 | 49 | )); 50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; 51 | 52 | const SelectScrollDownButton = React.forwardRef< 53 | React.ElementRef, 54 | React.ComponentPropsWithoutRef 55 | >(({ className, ...props }, ref) => ( 56 | 64 | 65 | 66 | )); 67 | SelectScrollDownButton.displayName = 68 | SelectPrimitive.ScrollDownButton.displayName; 69 | 70 | const SelectContent = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >(({ className, children, position = "popper", ...props }, ref) => ( 74 | 75 | 86 | 87 | 94 | {children} 95 | 96 | 97 | 98 | 99 | )); 100 | SelectContent.displayName = SelectPrimitive.Content.displayName; 101 | 102 | const SelectLabel = React.forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, ...props }, ref) => ( 106 | 111 | )); 112 | SelectLabel.displayName = SelectPrimitive.Label.displayName; 113 | 114 | const SelectItem = React.forwardRef< 115 | React.ElementRef, 116 | React.ComponentPropsWithoutRef 117 | >(({ className, children, ...props }, ref) => ( 118 | 126 | 127 | 128 | 129 | 130 | 131 | {children} 132 | 133 | )); 134 | SelectItem.displayName = SelectPrimitive.Item.displayName; 135 | 136 | const SelectSeparator = React.forwardRef< 137 | React.ElementRef, 138 | React.ComponentPropsWithoutRef 139 | >(({ className, ...props }, ref) => ( 140 | 145 | )); 146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 147 | 148 | export { 149 | Select, 150 | SelectGroup, 151 | SelectValue, 152 | SelectTrigger, 153 | SelectContent, 154 | SelectLabel, 155 | SelectItem, 156 | SelectSeparator, 157 | SelectScrollUpButton, 158 | SelectScrollDownButton, 159 | }; 160 | -------------------------------------------------------------------------------- /app/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<"textarea"> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |