├── src ├── app │ ├── globals.css │ ├── providers.tsx │ ├── api │ │ └── replicate │ │ │ └── webhook │ │ │ └── route.ts │ ├── layout.tsx │ ├── dream │ │ └── [id] │ │ │ ├── page.tsx │ │ │ └── dream.tsx │ └── page.tsx ├── assets │ └── fonts │ │ └── CalSans-SemiBold.otf ├── server │ ├── root.ts │ ├── trpc.ts │ └── routes │ │ └── replicate.ts ├── env.mjs ├── components │ ├── max-width.tsx │ ├── ux │ │ ├── imagine-record.tsx │ │ ├── imagine-form.tsx │ │ └── imagine-card.tsx │ └── ui │ │ ├── separator.tsx │ │ ├── toaster.tsx │ │ ├── fade-in.tsx │ │ ├── button.tsx │ │ ├── use-toast.ts │ │ ├── toast.tsx │ │ └── dropdown-menu.tsx ├── pages │ └── api │ │ └── trpc │ │ └── [trpc].ts └── lib │ ├── utils.ts │ ├── replicate.ts │ ├── providers │ ├── cars-provider.tsx │ └── trpc-provider.tsx │ └── gen.ts ├── README.md ├── postcss.config.js ├── next.config.js ├── components.json ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── tailwind.config.ts ├── tsconfig.json ├── package.json └── pnpm-lock.yaml /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DreamWheels 2 | 3 | An AI-powered tool to generate classic and vintage cars! 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/fonts/CalSans-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exception/dream-wheels/master/src/assets/fonts/CalSans-SemiBold.otf -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ['95j9dxyjuiyxt9vi.public.blob.vercel-storage.com'], 5 | }, 6 | }; 7 | 8 | module.exports = nextConfig; 9 | -------------------------------------------------------------------------------- /src/server/root.ts: -------------------------------------------------------------------------------- 1 | import { replicateRoutes } from "./routes/replicate"; 2 | import { createTRPCRouter } from "./trpc"; 3 | 4 | export const appRouter = createTRPCRouter({ 5 | replicate: replicateRoutes 6 | }); 7 | 8 | export type AppRouter = typeof appRouter; -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /src/env.mjs: -------------------------------------------------------------------------------- 1 | import { createEnv } from '@t3-oss/env-nextjs'; 2 | import { z } from 'zod'; 3 | 4 | export const env = createEnv({ 5 | server: { 6 | REPLICATE_TOKEN: z.string(), 7 | NODE_ENV: z.enum(['development', 'test', 'production']), 8 | }, 9 | 10 | runtimeEnv: { 11 | REPLICATE_TOKEN: process.env.REPLICATE_TOKEN, 12 | NODE_ENV: process.env.NODE_ENV, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /.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 | .env -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { CarsProvider } from '@/lib/providers/cars-provider'; 4 | import { TrpcProvider } from '@/lib/providers/trpc-provider'; 5 | import { Analytics } from '@vercel/analytics/react'; 6 | 7 | const Providers = ({ children }: React.PropsWithChildren) => { 8 | return ( 9 | <> 10 | 11 | {children} 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default Providers; 19 | -------------------------------------------------------------------------------- /src/components/max-width.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | 7 | const MaxWidthContainer = ({ 8 | className, 9 | children, 10 | }: React.PropsWithChildren) => { 11 | return ( 12 |
18 | {children} 19 |
20 | ); 21 | }; 22 | 23 | export default MaxWidthContainer; 24 | -------------------------------------------------------------------------------- /src/server/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from "@trpc/server"; 2 | import superjson from "superjson"; 3 | import { ZodError } from "zod"; 4 | 5 | 6 | const t = initTRPC.context().create({ 7 | transformer: superjson, 8 | errorFormatter({ shape, error }) { 9 | return { 10 | ...shape, 11 | data: { 12 | ...shape.data, 13 | zodError: 14 | error.cause instanceof ZodError ? error.cause.flatten() : null, 15 | }, 16 | }; 17 | }, 18 | }); 19 | 20 | export const createTRPCRouter = t.router; 21 | 22 | export const publicProcedure = t.procedure; 23 | -------------------------------------------------------------------------------- /src/components/ux/imagine-record.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useCarPovider } from '@/lib/providers/cars-provider'; 4 | import { FadeInStagger } from '../ui/fade-in'; 5 | import ImagineCard from './imagine-card'; 6 | 7 | const ImagineRecord = () => { 8 | const { record } = useCarPovider(); 9 | 10 | return ( 11 | 12 | {record.map((entry) => ( 13 | 14 | ))} 15 | 16 | ); 17 | }; 18 | 19 | export default ImagineRecord; 20 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { createNextApiHandler } from "@trpc/server/adapters/next"; 2 | import { env } from "@/env.mjs"; 3 | import { appRouter } from "@/server/root"; 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: { 8 | sizeLimit: '4mb', 9 | }, 10 | }, 11 | } 12 | 13 | export default createNextApiHandler({ 14 | router: appRouter, 15 | onError: 16 | env.NODE_ENV === "development" 17 | ? ({ path, error }) => { 18 | console.error( 19 | `❌ tRPC failed on ${path ?? ""}: ${error.message}` 20 | ); 21 | } 22 | : undefined, 23 | }); 24 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 8 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 9 | ], 10 | theme: { 11 | extend: { 12 | fontFamily: { 13 | display: ['var(--font-cal-sans)'], 14 | default: ['var(--font-inter)', 'system-ui', 'sans-serif'], 15 | }, 16 | }, 17 | }, 18 | plugins: [require("tailwindcss-animate")], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /src/server/routes/replicate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createTRPCRouter, publicProcedure } from "../trpc"; 3 | import { generateCar } from "@/lib/replicate"; 4 | import { kv } from "@vercel/kv"; 5 | 6 | export const replicateRoutes = createTRPCRouter({ 7 | generate: publicProcedure.input(z.object({ 8 | prompt: z.string() 9 | })).mutation(async ({ input }) => { 10 | const id = await generateCar(input.prompt); 11 | 12 | return { id, prompt: input.prompt }; 13 | }), 14 | get: publicProcedure.input(z.object({ 15 | id: z.string() 16 | })).query(({ input }) => { 17 | return kv.hgetall<{ id: string; url?: string; prompt: string }>(input.id); 18 | }) 19 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge"; 3 | import { customAlphabet } from "nanoid"; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)) 7 | } 8 | 9 | export const nanoid = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 10); 10 | 11 | export const IS_ON_VERCEL = !!process.env.VERCEL_URL; 12 | export const WEBHOOK_URL = IS_ON_VERCEL ? `https://${process.env.VERCEL_URL}/api/replicate/webhook` : `${process.env.NGROK_URL}/api/replicate/webhook`; 13 | 14 | export const delay = (ms: number): Promise => { 15 | return new Promise((resolve) => { 16 | setTimeout(() => { 17 | resolve(); 18 | }, ms); 19 | }); 20 | }; -------------------------------------------------------------------------------- /src/app/api/replicate/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import { kv } from '@vercel/kv'; 2 | import { put } from '@vercel/blob'; 3 | import { NextResponse } from 'next/server'; 4 | 5 | export const POST = async (req: Request) => { 6 | const searchParams = new URL(req.url).searchParams; 7 | const id = searchParams.get('id') as string; 8 | 9 | const body = await req.json(); 10 | const { output } = body; 11 | 12 | if (!output) { 13 | return new Response('Invalid Replicate output', { status: 400 }); 14 | } 15 | 16 | const file = await fetch(output[0]).then((res) => res.blob()); 17 | 18 | const { url } = await put(`${id}.png`, file, { access: 'public' }); 19 | 20 | await kv.hset(id, { url }); 21 | 22 | return NextResponse.json({ ok: true }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /src/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast" 11 | import { useToast } from "@/components/ui/use-toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css'; 2 | import type { Metadata } from 'next'; 3 | import { Inter } from 'next/font/google'; 4 | import localFont from 'next/font/local'; 5 | import { cn } from '@/lib/utils'; 6 | import Providers from './providers'; 7 | import { Toaster } from '@/components/ui/toaster'; 8 | 9 | const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }); 10 | 11 | const calSans = localFont({ 12 | src: '../assets/fonts/CalSans-SemiBold.otf', 13 | variable: '--font-cal-sans', 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: 'Create Next App', 18 | description: 'Generated by create next app', 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: { 24 | children: React.ReactNode; 25 | }) { 26 | return ( 27 | 28 | 29 | {/*
*/} 30 |
31 | {children} 32 |
33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/dream/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { kv } from '@vercel/kv'; 2 | import { notFound } from 'next/navigation'; 3 | import Dream from './dream'; 4 | import { Metadata } from 'next'; 5 | 6 | interface Props { 7 | params: { 8 | id: string; 9 | }; 10 | } 11 | 12 | const getDream = (id: string) => { 13 | return kv.hgetall<{ id: string; url?: string; prompt: string }>(id); 14 | }; 15 | 16 | export const generateMetadata = async ({ 17 | params, 18 | }: Props): Promise => { 19 | const dream = await getDream(params.id); 20 | if (!dream || !dream.url) return undefined; 21 | 22 | const title = `Dream #${dream.id}`; 23 | const description = dream.prompt; 24 | 25 | return { 26 | title, 27 | description, 28 | openGraph: { 29 | title, 30 | description, 31 | images: [dream.url], 32 | }, 33 | twitter: { 34 | card: 'summary_large_image', 35 | creator: '@erosemberg_', 36 | title, 37 | description, 38 | images: [dream.url], 39 | }, 40 | }; 41 | }; 42 | 43 | const DreamPage = async ({ params }: Props) => { 44 | const dream = await getDream(params.id); 45 | 46 | if (!dream) { 47 | notFound(); 48 | } 49 | 50 | return ; 51 | }; 52 | 53 | export default DreamPage; 54 | -------------------------------------------------------------------------------- /src/lib/replicate.ts: -------------------------------------------------------------------------------- 1 | import Replicate from 'replicate'; 2 | import { env } from '@/env.mjs'; 3 | import { WEBHOOK_URL, delay, nanoid } from './utils'; 4 | import { generateReplicatePrompt } from './gen'; 5 | import { kv } from '@vercel/kv'; 6 | 7 | const replicate = new Replicate({ 8 | auth: env.REPLICATE_TOKEN, 9 | }); 10 | 11 | export const generateCar = async (prompt: string) => { 12 | const promptId = nanoid(); 13 | const replicatePrompt = generateReplicatePrompt(prompt); 14 | 15 | await Promise.all([ 16 | kv.hset(promptId, { 17 | id: promptId, 18 | prompt: replicatePrompt, 19 | }), 20 | replicate.predictions.create({ 21 | version: 22 | '8beff3369e81422112d93b89ca01426147de542cd4684c244b673b105188fe5f', 23 | input: { 24 | prompt: replicatePrompt, 25 | negative_prompt: 26 | 'deformed, motionless, out of frame, painting, unreal, drivers, people, persons, companions, animals, visible license plate, readable license plate, number plate, morphed headlights, deformed roads, duplicated headlights, deformed wheels, rear view, tail lights', 27 | }, 28 | webhook_events_filter: ['completed'], 29 | webhook: `${WEBHOOK_URL}?id=${promptId}` 30 | }), 31 | ]); 32 | 33 | await delay(1_500); 34 | 35 | return promptId; 36 | }; 37 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import MaxWidthContainer from '@/components/max-width'; 2 | import { FadeIn, FadeInStagger } from '@/components/ui/fade-in'; 3 | import ImagineForm from '@/components/ux/imagine-form'; 4 | import ImagineRecord from '@/components/ux/imagine-record'; 5 | import { Github } from 'lucide-react'; 6 | import { Metadata } from 'next'; 7 | import Link from 'next/link'; 8 | 9 | export const metadata: Metadata = { 10 | title: 'Dream Wheels', 11 | description: 'AI-powered tool to generate your dream vintage/classic car', 12 | }; 13 | 14 | const Home = () => { 15 | return ( 16 | 17 | 21 | 22 |

23 | Dream Wheels. 24 |

25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default Home; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vintage-cars", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^3.3.1", 13 | "@radix-ui/react-dropdown-menu": "^2.0.6", 14 | "@radix-ui/react-separator": "^1.0.3", 15 | "@radix-ui/react-slot": "^1.0.2", 16 | "@radix-ui/react-toast": "^1.1.5", 17 | "@t3-oss/env-nextjs": "^0.6.1", 18 | "@tanstack/react-query": "^4.35.3", 19 | "@tanstack/react-query-devtools": "^4.35.3", 20 | "@trpc/client": "^10.38.4", 21 | "@trpc/react-query": "^10.38.4", 22 | "@trpc/server": "^10.38.4", 23 | "@vercel/analytics": "^1.0.2", 24 | "@vercel/blob": "^0.12.5", 25 | "@vercel/kv": "^0.2.3", 26 | "class-variance-authority": "^0.7.0", 27 | "clsx": "^2.0.0", 28 | "framer-motion": "^10.16.4", 29 | "lucide-react": "^0.279.0", 30 | "nanoid": "^5.0.1", 31 | "next": "latest", 32 | "radash": "^11.0.0", 33 | "react": "latest", 34 | "react-dom": "latest", 35 | "react-hook-form": "^7.46.1", 36 | "react-medium-image-zoom": "^5.1.8", 37 | "replicate": "^0.18.1", 38 | "superjson": "^1.13.1", 39 | "tailwind-merge": "^1.14.0", 40 | "tailwindcss-animate": "^1.0.7", 41 | "zod": "^3.22.2" 42 | }, 43 | "devDependencies": { 44 | "@types/node": "latest", 45 | "@types/react": "latest", 46 | "@types/react-dom": "latest", 47 | "autoprefixer": "latest", 48 | "postcss": "latest", 49 | "tailwindcss": "latest", 50 | "typescript": "latest" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/providers/cars-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | 3 | export interface Imagination { 4 | id: string; 5 | prompt: string; 6 | url?: string; 7 | } 8 | 9 | type ContextProps = { 10 | record: Imagination[]; 11 | addEntry: (entry: Imagination) => void; 12 | updateEntry: (id: string, entry: Imagination) => void; 13 | }; 14 | 15 | const Context = React.createContext({ 16 | record: [], 17 | addEntry: () => void 0, 18 | updateEntry: () => void 0, 19 | }); 20 | 21 | export const CarsProvider = ({ children }: React.PropsWithChildren) => { 22 | const [record, setRecord] = useState([]); 23 | 24 | const addEntry = (imagination: Imagination) => { 25 | setRecord((prevRecord) => [imagination, ...prevRecord]); 26 | }; 27 | 28 | const updateEntry = (id: string, update: Imagination) => { 29 | setRecord((prevRecord) => { 30 | return prevRecord.map((entry) => { 31 | if (entry.id === id) { 32 | return update; 33 | } 34 | return entry; 35 | }); 36 | }); 37 | }; 38 | 39 | return ( 40 | 47 | {children} 48 | 49 | ); 50 | }; 51 | 52 | export const useCarPovider = () => { 53 | const ctx = useContext(Context); 54 | if (!ctx) { 55 | throw new Error('Must be used within a CarsProvider'); 56 | } 57 | return ctx; 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/ui/fade-in.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createContext, useContext } from 'react'; 4 | import { motion, useReducedMotion } from 'framer-motion'; 5 | 6 | const FadeInStaggerContext = createContext(false); 7 | 8 | const viewport = { once: true, margin: '0px 0px -200px' }; 9 | 10 | export const FadeIn = ( 11 | props: React.ComponentPropsWithoutRef, 12 | ) => { 13 | let shouldReduceMotion = useReducedMotion(); 14 | let isInStaggerGroup = useContext(FadeInStaggerContext); 15 | 16 | return ( 17 | 32 | ); 33 | }; 34 | 35 | export const FadeInStagger = ({ 36 | faster = false, 37 | ...props 38 | }: React.ComponentPropsWithoutRef & { 39 | faster?: boolean; 40 | }) => { 41 | return ( 42 | 43 | 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/app/dream/[id]/dream.tsx: -------------------------------------------------------------------------------- 1 | import MaxWidthContainer from '@/components/max-width'; 2 | import { FadeIn, FadeInStagger } from '@/components/ui/fade-in'; 3 | import { Imagination } from '@/lib/providers/cars-provider'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | 7 | interface Props { 8 | dream: Imagination; 9 | } 10 | 11 | const Dream = ({ dream }: Props) => { 12 | return ( 13 | 14 | 18 | 19 | 20 |

21 | Dream Wheels. 22 |

23 | 24 |
25 | 26 |

27 | Imagine... {dream.prompt.toLowerCase()} 28 |

29 |
30 | 31 | {dream.id} 40 | 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default Dream; 47 | -------------------------------------------------------------------------------- /src/lib/providers/trpc-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { httpBatchLink, getFetch, loggerLink } from '@trpc/client'; 5 | import { useState } from 'react'; 6 | import superjson from 'superjson'; 7 | import { createTRPCReact } from '@trpc/react-query'; 8 | import { type AppRouter } from '@/server/root'; 9 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 10 | 11 | type trpcClient = ReturnType>; 12 | export const trpc: trpcClient = createTRPCReact(); 13 | 14 | export const TrpcProvider: React.FC<{ children: React.ReactNode }> = ({ 15 | children, 16 | }) => { 17 | const [queryClient] = useState( 18 | () => 19 | new QueryClient({ 20 | defaultOptions: { 21 | queries: { staleTime: 5000, refetchOnWindowFocus: false }, 22 | }, 23 | }), 24 | ); 25 | 26 | const [trpcClient] = useState(() => 27 | trpc.createClient({ 28 | links: [ 29 | loggerLink({ 30 | enabled: () => process.env.NODE_ENV !== 'production', 31 | }), 32 | httpBatchLink({ 33 | url: '/api/trpc', 34 | fetch: async (input, init?) => { 35 | const fetch = getFetch(); 36 | return fetch(input, { 37 | ...init, 38 | credentials: 'include', 39 | }); 40 | }, 41 | }), 42 | ], 43 | transformer: superjson, 44 | }), 45 | ); 46 | return ( 47 | 48 | 49 | {/* */} 53 | {children} 54 | 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/lib/gen.ts: -------------------------------------------------------------------------------- 1 | import { draw } from 'radash'; 2 | 3 | interface PromptOptions { 4 | color?: string; 5 | era?: string; 6 | location?: string; 7 | type?: string; 8 | origin?: string; 9 | } 10 | 11 | const staticParts = [ 12 | 'Cinematic', 13 | 'dramatic', 14 | 'in motion', 15 | 'realistic', 16 | 'ultra high-quality background and roads', 17 | ]; 18 | 19 | export const generatePrompt = ({ 20 | color = 'grey', 21 | era = '1960s', 22 | location = 'sahara desert', 23 | type = 'rally', 24 | origin = 'british', 25 | }: PromptOptions): string => { 26 | const prompt = `A vintage, ${color}, ${origin} from the ${era}, ${type} through ${location}`; 27 | 28 | return prompt; 29 | }; 30 | 31 | export const generateReplicatePrompt = (prompt: string) => { 32 | return `${prompt}. ${staticParts.join( 33 | ', ', 34 | )}. Picture taken from the left hand side with a DSLR camera, medium grain. Vehicle must be centered within the surroundings and facing the front.`; 35 | }; 36 | 37 | export const generatePlaceholder = (): PromptOptions => { 38 | const colors = [ 39 | 'english green', 40 | 'silver', 41 | 'orange', 42 | 'midnight black', 43 | 'white', 44 | 'cream', 45 | 'red', 46 | 'maroon', 47 | 'mustard color' 48 | ]; 49 | const era = [ 50 | '1920s', 51 | '1930s', 52 | '1940s', 53 | '1950s', 54 | '1960s', 55 | '1970s', 56 | '1980s', 57 | '1990s', 58 | 'pre-war era', 59 | ]; 60 | const locations = [ 61 | 'the sahara desert', 62 | 'the french alps', 63 | 'the argentine patagonia', 64 | 'the antarctic', 65 | 'greenland', 66 | 'a national park', 67 | 'a rainforest', 68 | 'the amazonian rainforest', 69 | 'egypt with pyramids', 70 | 'a vietnamese jungle', 71 | 'a bridge over the potomac', 72 | 'a road next to the nile', 73 | 'the scottish highlands', 74 | 'edinbrugh castle', 75 | 'buenos aires\' streets' 76 | ]; 77 | const type = [ 78 | 'rally', 79 | 'joy-ride', 80 | 'driving at midnight', 81 | 'rainy drive', 82 | 'snowing drive', 83 | 'driving at sunset', 84 | 'driving at sunrise', 85 | 'driving during a starry night' 86 | ]; 87 | const origin = [ 88 | 'american car', 89 | 'american suv', 90 | 'american truck', 91 | 'british car', 92 | 'german car', 93 | 'japanese car', 94 | 'french car', 95 | 'italian car', 96 | 'american sports car', 97 | 'british sports car', 98 | 'german sports car', 99 | 'italian sports car', 100 | 'japanese sports car' 101 | ]; 102 | 103 | return { 104 | color: draw(colors)!, 105 | era: draw(era)!, 106 | location: draw(locations)!, 107 | type: draw(type)!, 108 | origin: draw(origin)!, 109 | }; 110 | }; 111 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | import { Loader2 } from 'lucide-react'; 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex gap-x-2 items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300', 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | 'bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90', 14 | destructive: 15 | 'bg-red-500 text-neutral-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90', 16 | outline: 17 | 'border border-neutral-200 bg-white hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50', 18 | secondary: 19 | 'bg-neutral-100 text-neutral-900 hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80', 20 | ghost: 'hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50', 21 | link: 'text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50', 22 | pink: 'bg-pink text-white hover:bg-pink/90', 23 | }, 24 | size: { 25 | default: 'h-10 px-4 py-2', 26 | sm: 'h-9 rounded-md px-3', 27 | lg: 'h-10 rounded-md px-8', 28 | xl: 'h-10 rounded-md px-12', 29 | icon: 'h-10 w-10', 30 | }, 31 | }, 32 | defaultVariants: { 33 | variant: 'default', 34 | size: 'default', 35 | }, 36 | }, 37 | ); 38 | 39 | export interface ButtonProps 40 | extends React.PropsWithChildren< 41 | React.ButtonHTMLAttributes 42 | >, 43 | VariantProps { 44 | loading?: boolean; 45 | icon?: React.ReactNode; 46 | } 47 | 48 | const Button = React.forwardRef( 49 | ( 50 | { 51 | className, 52 | variant, 53 | size, 54 | loading = false, 55 | children, 56 | disabled, 57 | icon, 58 | ...props 59 | }, 60 | ref, 61 | ) => { 62 | return ( 63 | 73 | ); 74 | }, 75 | ); 76 | Button.displayName = 'Button'; 77 | 78 | export { Button, buttonVariants }; 79 | -------------------------------------------------------------------------------- /src/components/ux/imagine-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import { generatePlaceholder, generatePrompt } from '@/lib/gen'; 5 | import { useCarPovider } from '@/lib/providers/cars-provider'; 6 | import { trpc } from '@/lib/providers/trpc-provider'; 7 | import { zodResolver } from '@hookform/resolvers/zod'; 8 | import { RefreshCcw, Sparkles } from 'lucide-react'; 9 | import { useForm } from 'react-hook-form'; 10 | import { z } from 'zod'; 11 | import { useToast } from '../ui/use-toast'; 12 | 13 | const formSchema = z.object({ 14 | prompt: z.string().min(10).max(200), 15 | }); 16 | 17 | const ImagineForm = () => { 18 | const { addEntry } = useCarPovider(); 19 | const { toast } = useToast(); 20 | 21 | const generateCar = trpc.replicate.generate.useMutation({ 22 | onSuccess: ({ id, prompt }) => { 23 | addEntry({ id, prompt }); 24 | form.setValue('prompt', generatePrompt(generatePlaceholder())); 25 | }, 26 | }); 27 | 28 | const form = useForm>({ 29 | // @ts-ignore 30 | resolver: zodResolver(formSchema), 31 | defaultValues: { 32 | prompt: generatePrompt(generatePlaceholder()), 33 | }, 34 | }); 35 | 36 | const onSubmit = async (form: z.infer) => { 37 | const prompt = form.prompt.toLowerCase(); 38 | const isValidCarPrompt = 39 | prompt.includes('car') || 40 | prompt.includes('vehicle') || 41 | prompt.includes('bike') || 42 | prompt.includes('motorcycle') || 43 | prompt.includes('truck') || 44 | prompt.includes('suv'); 45 | 46 | if (!isValidCarPrompt) { 47 | toast({ 48 | variant: 'destructive', 49 | title: 'Invalid Prompt', 50 | description: 'Prompts must be related to cars or motorcycles.', 51 | }); 52 | 53 | return; 54 | } 55 | 56 | generateCar.mutate({ 57 | prompt: form.prompt, 58 | }); 59 | }; 60 | 61 | return ( 62 |
63 |
64 |