├── .eslintrc.json ├── .example.env ├── .gitignore ├── .prettierrc.js ├── README.md ├── app ├── api │ └── generate │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx ├── start │ ├── [id] │ │ └── page.tsx │ └── page.tsx └── waitlist │ └── page.tsx ├── components.json ├── components ├── Body.tsx ├── CTA.tsx ├── Footer.tsx ├── GradientWrapper.tsx ├── Hero.tsx ├── NavLink.tsx ├── Navbar.tsx ├── PromptSuggestion.tsx ├── QrCard.tsx ├── Testimonials.tsx ├── ui │ ├── alert.tsx │ ├── button.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── loading-dots.module.css │ ├── loadingdots.tsx │ └── textarea.tsx └── v0logo.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── box.svg ├── next.svg ├── og-image.png └── vercel.svg ├── tailwind.config.js ├── tsconfig.json └── utils ├── ReplicateClient.ts ├── downloadQrCode.ts ├── env.ts ├── service.ts ├── types.ts └── utils.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next", 3 | "rules": { 4 | "no-unused-vars": "error" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.example.env: -------------------------------------------------------------------------------- 1 | ## Set up Replicate: https://replicate.com 2 | REPLICATE_API_KEY= 3 | 4 | ## Set up Vercel Blob: https://vercel.com/docs/storage/vercel-blob/quickstart 5 | BLOB_READ_WRITE_TOKEN= 6 | 7 | ## Set up Vercel KV: https://vercel.com/docs/storage/vercel-kv/quickstart 8 | KV_URL= 9 | KV_REST_API_URL= 10 | KV_REST_API_TOKEN= 11 | KV_REST_API_READ_ONLY_TOKEN= 12 | -------------------------------------------------------------------------------- /.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 | 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 36 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 80, 6 | tabWidth: 2, 7 | }; 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | QrGPT – Generate beautiful AI QR Codes in seconds. 3 |

qrGPT

4 |
5 | 6 |

7 | Generate beautiful AI QR Codes in seconds. Powered by Vercel and Replicate. 8 |

9 | 10 |

11 | 12 | Codium 13 | 14 |

15 | 16 |

17 | Tech Stack · 18 | Deploy Your Own · 19 | Authors · 20 | Credits 21 |

22 |
23 | 24 | ## Tech Stack 25 | 26 | - Next.js [App Router](https://nextjs.org/docs/app) 27 | - [Replicate](https://replicate.com/) for the AI model 28 | - [Vercel Blob](https://vercel.com/storage/blob) for image storage 29 | - [Vercel KV](https://vercel.com/storage/kv) for redis storage and rate limiting 30 | - [Shadcn UI](https://ui.shadcn.com/) for the component library 31 | 32 | ## Deploy Your Own 33 | 34 | You can deploy this template to Vercel with the button below: 35 | 36 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.fyi/qrGPT) 37 | 38 | Note that you'll need to: 39 | 40 | - Set up [Replicate](https://replicate.com) 41 | - Set up [Vercel KV](https://vercel.com/docs/storage/vercel-kv/quickstart) 42 | - Set up [Vercel Blob](https://vercel.com/docs/storage/vercel-blob/quickstart) 43 | 44 | ## Authors 45 | 46 | - Hassan El Mghari ([@nutlope](https://twitter.com/nutlope)) 47 | - Kevin Hou ([@kevinhou22](https://twitter.com/kevinhou22)) 48 | 49 | ## Credits 50 | 51 | - [Codeium](https://codeium.com?repo_name=nutlope%2Fqrgpt) and [v0](https://v0.dev/) for quick prototyping and AI autocomplete 52 | - [Spirals](https://spirals.vercel.app/) for great code patterns and some code (ty Steven) 53 | - [Lim Zi Yang](https://github.com/ZYLIM0702) for the original AI model 54 | -------------------------------------------------------------------------------- /app/api/generate/route.ts: -------------------------------------------------------------------------------- 1 | import { replicateClient } from '@/utils/ReplicateClient'; 2 | import { QrGenerateRequest, QrGenerateResponse } from '@/utils/service'; 3 | import { NextRequest } from 'next/server'; 4 | // import { Ratelimit } from '@upstash/ratelimit'; 5 | import { kv } from '@vercel/kv'; 6 | import { put } from '@vercel/blob'; 7 | import { nanoid } from '@/utils/utils'; 8 | 9 | /** 10 | * Validates a request object. 11 | * 12 | * @param {QrGenerateRequest} request - The request object to be validated. 13 | * @throws {Error} Error message if URL or prompt is missing. 14 | */ 15 | 16 | const validateRequest = (request: QrGenerateRequest) => { 17 | if (!request.url) { 18 | throw new Error('URL is required'); 19 | } 20 | if (!request.prompt) { 21 | throw new Error('Prompt is required'); 22 | } 23 | }; 24 | 25 | // const ratelimit = new Ratelimit({ 26 | // redis: kv, 27 | // // Allow 20 requests from the same IP in 1 day. 28 | // limiter: Ratelimit.slidingWindow(20, '1 d'), 29 | // }); 30 | 31 | export async function POST(request: NextRequest) { 32 | const reqBody = (await request.json()) as QrGenerateRequest; 33 | 34 | // const ip = request.ip ?? '127.0.0.1'; 35 | // const { success } = await ratelimit.limit(ip); 36 | 37 | // if (!success && process.env.NODE_ENV !== 'development') { 38 | // return new Response('Too many requests. Please try again after 24h.', { 39 | // status: 429, 40 | // }); 41 | // } 42 | 43 | try { 44 | validateRequest(reqBody); 45 | } catch (e) { 46 | if (e instanceof Error) { 47 | return new Response(e.message, { status: 400 }); 48 | } 49 | } 50 | 51 | const id = nanoid(); 52 | const startTime = performance.now(); 53 | 54 | let imageUrl = await replicateClient.generateQrCode({ 55 | url: reqBody.url, 56 | prompt: reqBody.prompt, 57 | qr_conditioning_scale: 2, 58 | num_inference_steps: 30, 59 | guidance_scale: 5, 60 | negative_prompt: 61 | 'Longbody, lowres, bad anatomy, bad hands, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, blurry', 62 | }); 63 | 64 | const endTime = performance.now(); 65 | const durationMS = endTime - startTime; 66 | 67 | // convert output to a blob object 68 | const file = await fetch(imageUrl).then((res) => res.blob()); 69 | 70 | // upload & store in Vercel Blob 71 | const { url } = await put(`${id}.png`, file, { access: 'public' }); 72 | 73 | await kv.hset(id, { 74 | prompt: reqBody.prompt, 75 | image: url, 76 | website_url: reqBody.url, 77 | model_latency: Math.round(durationMS), 78 | }); 79 | 80 | const response: QrGenerateResponse = { 81 | image_url: url, 82 | model_latency_ms: Math.round(durationMS), 83 | id: id, 84 | }; 85 | 86 | return new Response(JSON.stringify(response), { 87 | status: 200, 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/qrGPT/ff11bf70e3463f0a1f49fedfa7522bb6b83d5627/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .custom-screen { 6 | @apply max-w-screen-xl mx-auto px-4 md:px-8; 7 | } 8 | 9 | .gradient-border { 10 | border: 1px solid transparent; 11 | background: linear-gradient(white, white), 12 | linear-gradient( 13 | 25deg, 14 | rgba(209, 213, 219, 1), 15 | rgba(209, 213, 219, 1), 16 | rgba(0, 0, 0, 0.63), 17 | rgba(209, 213, 219, 1), 18 | rgba(209, 213, 219, 1) 19 | ); 20 | background-clip: padding-box, border-box; 21 | background-origin: padding-box, border-box; 22 | } 23 | 24 | @layer base { 25 | :root { 26 | --background: 0 0% 100%; 27 | --foreground: 222.2 84% 4.9%; 28 | 29 | --card: 0 0% 100%; 30 | --card-foreground: 222.2 84% 4.9%; 31 | 32 | --popover: 0 0% 100%; 33 | --popover-foreground: 222.2 84% 4.9%; 34 | 35 | --primary: 222.2 47.4% 11.2%; 36 | --primary-foreground: 210 40% 98%; 37 | 38 | --secondary: 210 40% 96.1%; 39 | --secondary-foreground: 222.2 47.4% 11.2%; 40 | 41 | --muted: 210 40% 96.1%; 42 | --muted-foreground: 215.4 16.3% 46.9%; 43 | 44 | --accent: 210 40% 96.1%; 45 | --accent-foreground: 222.2 47.4% 11.2%; 46 | 47 | --destructive: 0 84.2% 60.2%; 48 | --destructive-foreground: 210 40% 98%; 49 | 50 | --border: 214.3 31.8% 91.4%; 51 | --input: 214.3 31.8% 91.4%; 52 | --ring: 222.2 84% 4.9%; 53 | 54 | --radius: 0.5rem; 55 | } 56 | 57 | .dark { 58 | --background: 222.2 84% 4.9%; 59 | --foreground: 210 40% 98%; 60 | 61 | --card: 222.2 84% 4.9%; 62 | --card-foreground: 210 40% 98%; 63 | 64 | --popover: 222.2 84% 4.9%; 65 | --popover-foreground: 210 40% 98%; 66 | 67 | --primary: 210 40% 98%; 68 | --primary-foreground: 222.2 47.4% 11.2%; 69 | 70 | --secondary: 217.2 32.6% 17.5%; 71 | --secondary-foreground: 210 40% 98%; 72 | 73 | --muted: 217.2 32.6% 17.5%; 74 | --muted-foreground: 215 20.2% 65.1%; 75 | 76 | --accent: 217.2 32.6% 17.5%; 77 | --accent-foreground: 210 40% 98%; 78 | 79 | --destructive: 0 62.8% 30.6%; 80 | --destructive-foreground: 210 40% 98%; 81 | 82 | --border: 217.2 32.6% 17.5%; 83 | --input: 217.2 32.6% 17.5%; 84 | --ring: 212.7 26.8% 83.9%; 85 | } 86 | } 87 | 88 | @layer base { 89 | * { 90 | @apply border-border; 91 | } 92 | body { 93 | @apply bg-background text-foreground; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from '@/components/Navbar'; 2 | import './globals.css'; 3 | import type { Metadata } from 'next'; 4 | import { Inter } from 'next/font/google'; 5 | import Footer from '@/components/Footer'; 6 | import { Analytics } from '@vercel/analytics/react'; 7 | import PlausibleProvider from 'next-plausible'; 8 | 9 | const inter = Inter({ subsets: ['latin'] }); 10 | 11 | let title = 'QrGPT - QR Code Generator'; 12 | let description = 'Generate your AI QR Code in seconds'; 13 | let url = 'https://www.qrgpt.io'; 14 | let ogimage = 'https://www.qrgpt.io/og-image.png'; 15 | let sitename = 'qrGPT.io'; 16 | 17 | export const metadata: Metadata = { 18 | metadataBase: new URL(url), 19 | title, 20 | description, 21 | icons: { 22 | icon: '/favicon.ico', 23 | }, 24 | openGraph: { 25 | images: [ogimage], 26 | title, 27 | description, 28 | url: url, 29 | siteName: sitename, 30 | locale: 'en_US', 31 | type: 'website', 32 | }, 33 | twitter: { 34 | card: 'summary_large_image', 35 | images: [ogimage], 36 | title, 37 | description, 38 | }, 39 | }; 40 | 41 | export default function RootLayout({ 42 | children, 43 | }: { 44 | children: React.ReactNode; 45 | }) { 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | 53 |
{children}
54 | 55 |