├── .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 |
3 | qrGPT
4 |
5 |
6 |
7 | Generate beautiful AI QR Codes in seconds. Powered by Vercel and Replicate.
8 |
9 |
10 |
11 |
12 |
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 | [](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 |
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import CTA from '@/components/CTA';
2 | import GradientWrapper from '@/components/GradientWrapper';
3 | import Hero from '@/components/Hero';
4 |
5 | export default function Home() {
6 | return (
7 | <>
8 |
9 |
10 |
11 | >
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/app/start/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { kv } from '@vercel/kv';
2 | import { notFound } from 'next/navigation';
3 | import { Metadata } from 'next';
4 | import Body from '@/components/Body';
5 |
6 | async function getAllKv(id: string) {
7 | const data = await kv.hgetall<{
8 | prompt: string;
9 | image?: string;
10 | website_url?: string;
11 | model_latency?: string;
12 | }>(id);
13 |
14 | return data;
15 | }
16 |
17 | export async function generateMetadata({
18 | params,
19 | }: {
20 | params: {
21 | id: string;
22 | };
23 | }): Promise {
24 | const data = await getAllKv(params.id);
25 | if (!data) {
26 | return;
27 | }
28 |
29 | const title = `QrGPT: ${data.prompt}`;
30 | const description = `A QR code generated from qrGPT.io linking to: ${data.website_url}`;
31 | const image = data.image || 'https://qrGPT.io/og-image.png';
32 |
33 | return {
34 | title,
35 | description,
36 | openGraph: {
37 | title,
38 | description,
39 | images: [
40 | {
41 | url: image,
42 | },
43 | ],
44 | },
45 | twitter: {
46 | card: 'summary_large_image',
47 | title,
48 | description,
49 | images: [image],
50 | creator: '@nutlope',
51 | },
52 | };
53 | }
54 |
55 | export default async function Results({
56 | params,
57 | }: {
58 | params: {
59 | id: string;
60 | };
61 | }) {
62 | const data = await getAllKv(params.id);
63 | if (!data) {
64 | notFound();
65 | }
66 | return (
67 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/app/start/page.tsx:
--------------------------------------------------------------------------------
1 | import Body from '@/components/Body';
2 |
3 | export default function GeneratePage() {
4 | return