diff --git a/CLAUDE.md b/CLAUDE.md index 27588678..ac06687f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -114,3 +114,24 @@ GitHub Actions workflows in `.github/workflows/`: - `re-pull.yml` — triggers Portainer to redeploy latest image To release: tag with `git tag -a v0.1.x -m "..."` and push the tag. + +### Workflow for Code Changes +1. **Commit** existing changes before starting new work +2. **Create plan** at `MIND/PLAN/[plan-name].md` +3. **Create task** at `MIND/PLAN/[task-name].md` +4. **Execute the task** and update task progress +5. **Create summary** at `MIND/SUMMARY/[summary-name].md` when done +6. **Run build** (`bun run build`) to ensure no compile errors +7. **Fix any build errors** if they occur +8. **Commit** all changes AFTER successful build +9. **Update version** in `package.json` for every change +10. **Push** to new branch with format: `tasks/[task-name]/[what-is-being-done]/[date-time]` +11. **Push ke 2 Remote** - Push ke 2 remote origin dan deploy +12. **Merge ke Branch** - Merge ke branch target (biasanya `stg` untuk staging atau `prod` untuk production) ke 2 remote origin dan deploy + +### GitHub Workflows +1. **publish.yml**: Uses branch `main`, stack env and image tag matching version from `package.json`. +2. **re-pull.yml**: **Wait for `publish.yml` to complete successfully before running.** Uses branch `main`, stack env and stack name `desa-darmasaba`. + +### After Progress +- Always give option to continue to GitHub workflows or not diff --git a/prisma/migrations/20260423072135_add_stok_to_pasar_desa/migration.sql b/prisma/migrations/20260423072135_add_stok_to_pasar_desa/migration.sql new file mode 100644 index 00000000..3fe9282a --- /dev/null +++ b/prisma/migrations/20260423072135_add_stok_to_pasar_desa/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - You are about to drop the `ProdukUmkm` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `umkmId` to the `PasarDesa` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "PenjualanProduk" DROP CONSTRAINT "PenjualanProduk_produkId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProdukUmkm" DROP CONSTRAINT "ProdukUmkm_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProdukUmkm" DROP CONSTRAINT "ProdukUmkm_umkmId_fkey"; + +-- AlterTable +ALTER TABLE "KategoriProduk" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "PasarDesa" ADD COLUMN "stok" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "umkmId" TEXT NOT NULL, +ALTER COLUMN "rating" SET DEFAULT 0, +ALTER COLUMN "alamatUsaha" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT, +ALTER COLUMN "kontak" DROP NOT NULL; + +-- DropTable +DROP TABLE "ProdukUmkm"; + +-- AddForeignKey +ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_umkmId_fkey" FOREIGN KEY ("umkmId") REFERENCES "Umkm"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PenjualanProduk" ADD CONSTRAINT "PenjualanProduk_produkId_fkey" FOREIGN KEY ("produkId") REFERENCES "PasarDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/app/api/[[...slugs]]/_lib/fileStorage/_lib/create.ts b/src/app/api/[[...slugs]]/_lib/fileStorage/_lib/create.ts index e81ce2c1..d49505fa 100644 --- a/src/app/api/[[...slugs]]/_lib/fileStorage/_lib/create.ts +++ b/src/app/api/[[...slugs]]/_lib/fileStorage/_lib/create.ts @@ -2,10 +2,14 @@ import prisma from "@/lib/prisma"; import { Context } from "elysia"; import { nanoid } from "nanoid"; import sharp from "sharp"; +import zlib from "zlib"; import minio, { MINIO_BUCKET } from "@/lib/minio"; const fileStorageCreate = async (context: Context) => { - const body = (await context.body) as { name: string; file: File }; + const body = (await context.body) as { + name: string; + file: File; + }; const file = body.file; const name = body.name; @@ -16,49 +20,65 @@ const fileStorageCreate = async (context: Context) => { const isImage = file.type.startsWith("image/"); const isAudio = file.type.startsWith("audio/"); const category = isImage ? "image" : isAudio ? "audio" : "document"; - const prefix = category === "image" ? "image" : category === "audio" ? "audio" : "documents"; + const pathName = category === "image" ? "image" : category === "audio" ? "audio" : "document"; + + // Convert File ke Buffer const buffer = Buffer.from(await file.arrayBuffer()); let finalName = nanoid(); let finalMimeType = file.type; if (isImage) { - const mobileBuffer = await sharp(buffer) - .resize({ width: 720 }) - .webp({ quality: 80 }) - .toBuffer(); - + // Simpan sebagai WebP untuk kompresi maksimal const desktopBuffer = await sharp(buffer) .resize({ width: 1920, withoutEnlargement: true }) .webp({ quality: 80 }) .toBuffer(); - const mobileName = `${finalName}-mobile.webp`; const desktopName = `${finalName}-desktop.webp`; - await minio.putObject(MINIO_BUCKET, `${prefix}/${mobileName}`, mobileBuffer, mobileBuffer.length, { "Content-Type": "image/webp" }); - await minio.putObject(MINIO_BUCKET, `${prefix}/${desktopName}`, desktopBuffer, desktopBuffer.length, { "Content-Type": "image/webp" }); + // Upload to MinIO + await minio.putObject(MINIO_BUCKET, `${pathName}/${desktopName}`, desktopBuffer, desktopBuffer.length, { + "Content-Type": "image/webp", + }); + // Simpan metadata untuk versi desktop sebagai default finalName = desktopName; finalMimeType = "image/webp"; } else if (isAudio) { + // Simpan file audio tanpa kompresi const ext = file.name.split(".").pop() || "mp3"; finalName = `${finalName}.${ext}`; - await minio.putObject(MINIO_BUCKET, `${prefix}/${finalName}`, buffer, buffer.length, { "Content-Type": file.type }); + await minio.putObject(MINIO_BUCKET, `${pathName}/${finalName}`, buffer, buffer.length, { + "Content-Type": finalMimeType, + }); } else { - const ext = file.name.split(".").pop() || "bin"; - finalName = `${finalName}.${ext}`; - await minio.putObject(MINIO_BUCKET, `${prefix}/${finalName}`, buffer, buffer.length, { "Content-Type": file.type }); + // Jika file adalah PDF, simpan tanpa kompresi + if (file.type === "application/pdf") { + await minio.putObject(MINIO_BUCKET, `${pathName}/${finalName}`, buffer, buffer.length, { + "Content-Type": finalMimeType, + }); + } + // Jika file lain, kompres dengan gzip + else { + const gzBuffer = zlib.gzipSync(buffer); + const compressedName = `${finalName}.gz`; + await minio.putObject(MINIO_BUCKET, `${pathName}/${compressedName}`, gzBuffer, gzBuffer.length, { + "Content-Type": "application/gzip", + }); + finalName = compressedName; + finalMimeType = "application/gzip"; + } } const data = await prisma.fileStorage.create({ data: { name: finalName, realName: file.name, - path: prefix, + path: pathName, mimeType: finalMimeType, category, - link: isImage ? `/api/img/${finalName}` : `/api/fileStorage/findUnique/${finalName}`, + link: category === "image" ? `/api/img/${finalName}` : `/api/fileStorage/findUnique/${finalName}`, }, }); diff --git a/src/app/api/[[...slugs]]/_lib/img.ts b/src/app/api/[[...slugs]]/_lib/img.ts index f553cdd9..1da6ec57 100644 --- a/src/app/api/[[...slugs]]/_lib/img.ts +++ b/src/app/api/[[...slugs]]/_lib/img.ts @@ -3,6 +3,14 @@ import fs from "fs/promises"; import sharp from "sharp"; import minio, { MINIO_BUCKET } from "@/lib/minio"; +const MIME_MAP: Record = { + ".webp": "image/webp", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".gif": "image/gif", +}; + async function img({ name, ROOT, @@ -14,12 +22,12 @@ async function img({ }) { const noImage = path.join(ROOT, "public/no-image.jpg"); const ext = path.extname(name).toLowerCase(); + const contentType = MIME_MAP[ext]; - if (![".jpg", ".jpeg", ".png", ".webp"].includes(ext)) { + if (!contentType) { console.warn(`Ekstensi file tidak didukung: ${ext}`); const buffer = await fs.readFile(noImage); - const uint8Array = new Uint8Array(buffer); - return new Response(new Blob([uint8Array], { type: 'image/jpeg' }), { + return new Response(new Uint8Array(buffer), { headers: { "Content-Type": "image/jpeg" }, }); } @@ -32,15 +40,17 @@ async function img({ } const buffer = Buffer.concat(chunks); - const metadata = await sharp(buffer).metadata(); - const resized = await sharp(buffer) - .resize(size || metadata.width) - .toBuffer(); + let resized: Buffer; + if (size) { + resized = await sharp(buffer).resize(size).toBuffer(); + } else { + resized = buffer; + } return new Response(new Uint8Array(resized), { headers: { "Cache-Control": "public, max-age=3600, stale-while-revalidate=600", - "Content-Type": "image/jpeg", + "Content-Type": contentType, }, }); } catch (error) {