Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: WIP upload recipe image #643

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ DIRECT_UNPOOLED_URL=postgresql://db_user:db_user_pass@localhost:6033/app_db
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=

# uploadthing for file uploads
UPLOADTHING_TOKEN=

# upstash for redis
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# -- Optional --

# sentry for error tracking
Expand Down
60 changes: 31 additions & 29 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,23 @@
"prisma:push": "prisma db push"
},
"dependencies": {
"@clerk/nextjs": "^5.2.8",
"@clerk/themes": "^1.7.17",
"@clerk/types": "^3.65.2",
"@clerk/nextjs": "^5.6.0",
"@clerk/themes": "^1.7.18",
"@clerk/types": "^3.65.3",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@headlessui/tailwindcss": "^0.2.1",
"@hookform/resolvers": "^3.9.0",
"@mantine/hooks": "^7.11.2",
"@neondatabase/serverless": "^0.9.4",
"@mantine/hooks": "^7.12.2",
"@neondatabase/serverless": "^0.9.5",
"@opentelemetry/api": "1.9.0",
"@pinecone-database/pinecone": "^1.1.3",
"@planetscale/database": "^1.18.0",
"@prisma/adapter-neon": "^5.17.0",
"@prisma/adapter-planetscale": "^5.17.0",
"@prisma/client": "^5.17.0",
"@planetscale/database": "^1.19.0",
"@prisma/adapter-neon": "^5.19.1",
"@prisma/adapter-planetscale": "^5.19.1",
"@prisma/client": "^5.19.1",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
Expand All @@ -65,17 +65,19 @@
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"@sentry/nextjs": "^8.20.0",
"@sentry/nextjs": "^8.30.0",
"@t3-oss/env-nextjs": "^0.6.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.13",
"@tanstack/react-query": "^5.51.15",
"@tanstack/react-table": "^8.19.3",
"@tremor/react": "^3.17.4",
"@types/node": "^20.14.13",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/react-query": "^5.56.2",
"@tanstack/react-table": "^8.20.5",
"@tremor/react": "^3.18.2",
"@types/node": "^20.16.5",
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
"@uploadthing/react": "^5.7.0",
"@uploadthing/react": "^7.0.2",
"@upstash/ratelimit": "^2.0.3",
"@upstash/redis": "^1.34.0",
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",
"ai": "^2.2.37",
Expand All @@ -91,19 +93,19 @@
"lucide-react": "^0.279.0",
"next": "14.2.5",
"next-contentlayer2": "^0.4.6",
"next-safe-action": "^7.4.3",
"next-safe-action": "^7.9.3",
"next-themes": "^0.2.1",
"openai": "^4.53.2",
"openai": "^4.62.1",
"pdf-parse": "^1.1.1",
"postcss": "8.4.31",
"posthog-js": "^1.150.1",
"posthog-node": "^4.0.1",
"prisma": "^5.17.0",
"posthog-js": "^1.161.6",
"posthog-node": "^4.2.0",
"prisma": "^5.19.1",
"react": "18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "18.3.1",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.52.1",
"react-hook-form": "^7.53.0",
"react-loading-skeleton": "^3.4.0",
"react-markdown": "^8.0.7",
"react-pdf": "^7.7.3",
Expand All @@ -114,12 +116,12 @@
"sonner": "^1.5.0",
"stripe": "^13.11.0",
"superjson": "^2.2.1",
"tailwind-merge": "^2.4.0",
"tailwind-merge": "^2.5.2",
"tailwindcss": "3.4.3",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.5.4",
"uploadthing": "^5.7.4",
"vaul": "^0.9.1",
"typescript": "^5.6.2",
"uploadthing": "^7.0.2",
"vaul": "^0.9.4",
"ws": "^8.18.0",
"zod": "^3.23.8"
},
Expand All @@ -129,7 +131,7 @@
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@types/eslint": "^8.56.11",
"@types/eslint": "^8.56.12",
"@types/lodash": "^4.17.7",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^6.21.0",
Expand All @@ -140,7 +142,7 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-tailwindcss": "^3.17.4",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.5",
"prettier-plugin-tailwindcss": "^0.6.6",
"rehype-pretty-code": "^0.10.2",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.0",
Expand Down
9 changes: 9 additions & 0 deletions apps/web/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ model Recipe {
ratings RecipeRating[]
ingredients IngredientsOnRecipes[]
steps Step[]
images StoredFile[]
}

model StoredFile {
id String @id @default(cuid())
name String
url String
recipe Recipe? @relation(fields: [recipeId], references: [id])
recipeId String?
}

model RecipeRating {
Expand Down
47 changes: 47 additions & 0 deletions apps/web/src/app/api/uploadthing/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ratelimit } from '@/lib/rate-limit';
import { currentUser } from '@clerk/nextjs/server';
import { createUploadthing, type FileRouter } from 'uploadthing/next';
import { UploadThingError } from 'uploadthing/server';

const f = createUploadthing();

// FileRouter for your app, can contain multiple FileRoutes
export const ourFileRouter = {
// Define as many FileRoutes as you like, each with a unique routeSlug
recipeImage: f({
image: { maxFileSize: '4MB', maxFileCount: 3 },
})
// Set permissions and file types for this FileRoute
.middleware(async ({ req }) => {
// This code runs on your server before upload

// Rate limit the upload
const ip = req.ip ?? '127.0.0.1';

const { success } = await ratelimit.limit(ip);

if (!success) {
throw new UploadThingError('Rate limit exceeded');
}

const user = await currentUser();

// If you throw, the user will not be able to upload
if (!user) throw new UploadThingError('Unauthorized');

// Whatever is returned here is accessible in onUploadComplete as `metadata`
return { userId: user.id };
})
// eslint-disable-next-line @typescript-eslint/require-await
.onUploadComplete(async ({ metadata, file }) => {
// This code RUNS ON YOUR SERVER after upload
console.log('Upload complete for userId:', metadata.userId);

console.log('file url', file.url);

// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
return { uploadedBy: metadata.userId };
}),
} satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;
10 changes: 10 additions & 0 deletions apps/web/src/app/api/uploadthing/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createRouteHandler } from 'uploadthing/next';
import { ourFileRouter } from './core';

// Export routes for Next App Router
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,

// Apply an (optional) custom config:
// config: { ... },
});
Loading