From 92837fb74fd90ebfb3e1d4f5d2e0ce894e55689a Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sun, 5 Jan 2025 18:04:37 +0100 Subject: [PATCH] feat: image previews --- README.md | 25 +++---------------- next.config.mjs | 7 +++++- package.json | 1 + .../20250105130053_add_preview/migration.sql | 8 ++++++ prisma/schema.prisma | 15 +++++------ src/app/(public)/post/[id]/page.tsx | 16 ++++++++---- src/app/globals.css | 4 +-- src/components/app/NavBar/NavBar.tsx | 3 +-- src/lib/form/actions.ts | 10 ++++++-- src/lib/form/zod.ts | 2 +- src/lib/hashImage.ts | 16 ++++++++++++ 11 files changed, 65 insertions(+), 42 deletions(-) create mode 100644 prisma/migrations/20250105130053_add_preview/migration.sql create mode 100644 src/lib/hashImage.ts diff --git a/README.md b/README.md index ee9e163..a52a9de 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,5 @@ -# Sr Izan Stack +# nextbooru -Sr Izan Stack is a next.js template which runs on modern technologies, with a focus on developer experience and ease-of-use. +Nextbooru is a simple image board software written with modern web technologies. It is designed to be fast and clean. -## The stack - -- Framework: [Next.js](https://nextjs.org/) -- Language: [TypeScript](https://www.typescriptlang.org/) -- Styling: [Tailwind CSS](https://tailwindcss.com/) -- UI Library: [shadcn/ui](https://ui.shadcn.com) -- Authentication: [Lucia](https://lucia-auth.com) -- Deployment: [Vercel](https://vercel.com) -- Database: [Supabase Postgres](https://supabase.com) with [Prisma](https://www.prisma.io/) - -## Why (insert tool here)? - -- **Next.js**: I like the next.js app router because it has a very good developer experience and it's very easy to use. -- **TypeScript**: Don't even need to explain why -- **Tailwind CSS**: I like the utility-first approach and the speed of development it provides -- **shadcn/ui**: Copy-pasting components is so fire (also is Radix UI) -- **Lucia**: The DevEX is amazing and it's very easy to use -- **Vercel**: Next.js and Vercel are like bread and butter, but it's a bit slow with the free tier. -- **MongoDB Atlas**: It has a very generous free tier, and I'm choosing NoSQL because Postgres hates me. -- **Prisma**: Even though there are solid competitors like Drizzle, Prisma is easy to use, understand, and fast enough for my use case. \ No newline at end of file +The software is very unstable and is not recommended for production use. For now, a testing server is available at [nextbooru.srizan.dev](https://nextbooru.srizan.dev). \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index b5bcefb..40d063c 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -3,7 +3,12 @@ const nextConfig = { webpack: (config) => { config.externals.push("@node-rs/argon2"); return config; - } + }, + experimental: { + serverActions: { + bodySizeLimit: '20mb' + } + } }; export default nextConfig; diff --git a/package.json b/package.json index c21a5bd..c846d99 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react": "19", "react-dom": "19", "react-hook-form": "^7.54.2", + "sharp": "^0.33.5", "sonner": "^1.4.41", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", diff --git a/prisma/migrations/20250105130053_add_preview/migration.sql b/prisma/migrations/20250105130053_add_preview/migration.sql new file mode 100644 index 0000000..61fa09a --- /dev/null +++ b/prisma/migrations/20250105130053_add_preview/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `previewHash` to the `Post` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Post" ADD COLUMN "previewHash" TEXT NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 958608c..55065e2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,11 +29,12 @@ model Session { } model Post { - id String @id @default(cuid()) - imageUrl String - caption String - createdAt DateTime @default(now()) - tags String[] - authorId String - author User @relation(references: [id], fields: [authorId], onDelete: Cascade) + id String @id @default(cuid()) + imageUrl String + caption String + createdAt DateTime @default(now()) + tags String[] + authorId String + author User @relation(references: [id], fields: [authorId], onDelete: Cascade) + previewHash String } diff --git a/src/app/(public)/post/[id]/page.tsx b/src/app/(public)/post/[id]/page.tsx index 779b9c6..fa86bde 100644 --- a/src/app/(public)/post/[id]/page.tsx +++ b/src/app/(public)/post/[id]/page.tsx @@ -10,7 +10,7 @@ export default async function Page({ params }: { params: Promise<{ id: string }> const { id } = await params; const post = await prisma.post.findUnique({ where: { id }, include: { author: true } }); if (!post) { - return
Post not found
; + return

Post not found

; } return ( @@ -23,9 +23,10 @@ export default async function Page({ params }: { params: Promise<{ id: string }> Post image
@@ -61,7 +62,12 @@ export default async function Page({ params }: { params: Promise<{ id: string }> Uploaded on: {post.createdAt.toLocaleString()}
- + +
+

Description

+

{post.caption}

+
+

Tags

diff --git a/src/app/globals.css b/src/app/globals.css index 173f5d0..2b4a147 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -38,7 +38,7 @@ --mantle: 220 22% 92%; - --radius: 0.5rem; + --radius: 0; } .dark { @@ -76,7 +76,7 @@ --mantle: 240 21.311% 11.961%; - --radius: 0.5rem; + --radius: 0; } } diff --git a/src/components/app/NavBar/NavBar.tsx b/src/components/app/NavBar/NavBar.tsx index d651644..329bfb8 100644 --- a/src/components/app/NavBar/NavBar.tsx +++ b/src/components/app/NavBar/NavBar.tsx @@ -24,8 +24,7 @@ import { ThemeSwitcher } from '../ThemeSwitcher/ThemeSwitcher'; export const links = [ { href: '/', name: 'Home' }, - { href: 'https://github.com/SrIzan10/stack', name: 'Github' }, - { href: '/protected', name: 'Protected route' }, + { href: '/upload', name: 'Upload' }, ]; function NavbarLinks() { diff --git a/src/lib/form/actions.ts b/src/lib/form/actions.ts index e46dbea..6beb7d4 100644 --- a/src/lib/form/actions.ts +++ b/src/lib/form/actions.ts @@ -5,6 +5,7 @@ import zodVerify from '../zodVerify'; import { uploadSchema } from './zod'; import prisma from '../db'; import { validateRequest } from '../auth'; +import hashImage from '../hashImage'; export async function create(prev: any, formData: FormData) { const { user } = await validateRequest(); @@ -18,18 +19,23 @@ export async function create(prev: any, formData: FormData) { const file = zod.data.file as File; - const buffer = new Uint8Array(await file.arrayBuffer()); + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + const uint = new Uint8Array(arrayBuffer); const filename = `${Date.now()}-${file.name.replaceAll(' ', '_')}`; - await writeFile(`./public/uploads/${filename}`, buffer).catch((e) => { + await writeFile(`./public/uploads/${filename}`, uint).catch((e) => { console.log(e); return { success: false, message: 'writing file' }; }); + const imagePreviewHash = await hashImage(buffer); + const dbCreate = await prisma.post.create({ data: { caption: zod.data.caption, tags: zod.data.tags.split(',').map((tag: string) => tag.trim()), imageUrl: `/uploads/${filename}`, + previewHash: imagePreviewHash, author: { connect: { id: user.id diff --git a/src/lib/form/zod.ts b/src/lib/form/zod.ts index f548710..fa505c0 100644 --- a/src/lib/form/zod.ts +++ b/src/lib/form/zod.ts @@ -16,7 +16,7 @@ export const uploadSchema = z.object({ file: z .any() .refine((file: File) => file?.name !== '', 'No file name found.') - .refine((file) => file.size < 5000000, 'Max size is 5MB.'), + .refine((file) => file.size < 20000000, 'Max size is 20MB.'), caption: z.string().min(1), tags: z.string().min(1), }); diff --git a/src/lib/hashImage.ts b/src/lib/hashImage.ts new file mode 100644 index 0000000..4563599 --- /dev/null +++ b/src/lib/hashImage.ts @@ -0,0 +1,16 @@ +import sharp from 'sharp'; +import crypto from 'crypto'; + +export default async function hashImage(imageBuffer: Buffer) { + try { + const previewBuffer = await sharp(imageBuffer, { limitInputPixels: Number.MAX_SAFE_INTEGER }) + .resize(32, 32) + .blur(4) + .toBuffer(); + + return previewBuffer.toString('base64'); + + } catch (error) { + throw new Error(`Failed to create image preview: ${error}`); + } +} \ No newline at end of file