From 1a74a1f683f31d25d77e3e4c9caa8d281bfba9fb Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 20 Apr 2026 17:44:36 +0800 Subject: [PATCH] feat(public-ui): implement public UMKM directory, detail and product catalog pages --- MIND/PLAN/umkm-module.md | 4 + MIND/SUMMARY/umkm-module-summary.md | 3 + package.json | 2 +- .../(dashboard)/_state/ekonomi/umkm/umkm.ts | 43 +++- .../(pages)/ekonomi/umkm/[id]/page.tsx | 120 +++++++++++ .../darmasaba/(pages)/ekonomi/umkm/page.tsx | 153 ++++++++++++++ .../(pages)/ekonomi/umkm/produk/page.tsx | 190 ++++++++++++++++++ src/con/navbar-list-menu.ts | 24 ++- 8 files changed, 529 insertions(+), 10 deletions(-) create mode 100644 src/app/darmasaba/(pages)/ekonomi/umkm/[id]/page.tsx create mode 100644 src/app/darmasaba/(pages)/ekonomi/umkm/page.tsx create mode 100644 src/app/darmasaba/(pages)/ekonomi/umkm/produk/page.tsx diff --git a/MIND/PLAN/umkm-module.md b/MIND/PLAN/umkm-module.md index 0afb2f03..8b89ce95 100644 --- a/MIND/PLAN/umkm-module.md +++ b/MIND/PLAN/umkm-module.md @@ -28,3 +28,7 @@ Implement UMKM, ProdukUmkm, and PenjualanProduk module with CRUD API and Dashboa - [x] Step 12: Implement Produk UI Page - [x] Step 13: Implement Penjualan UI Page - [x] Step 14: Register UI pages in Admin Menu +- [x] Step 15: Implement Public UMKM Directory Page +- [x] Step 16: Implement Public UMKM Detail Page +- [x] Step 17: Implement Public Product Catalog Page +- [x] Step 18: Register public pages in Navbar diff --git a/MIND/SUMMARY/umkm-module-summary.md b/MIND/SUMMARY/umkm-module-summary.md index 41e56f9e..0f72b5dd 100644 --- a/MIND/SUMMARY/umkm-module-summary.md +++ b/MIND/SUMMARY/umkm-module-summary.md @@ -8,6 +8,9 @@ - Implemented the Admin UI with a modern tab-based layout. - Created four main admin pages: Dashboard, Data UMKM, Produk, and Penjualan. - Registered the new UMKM module in the Admin Navigation Menu for all roles. +- Implemented the Public UI for citizens to browse local businesses. +- Created three public pages: Direktori UMKM, UMKM Detail, and Katalog Produk. +- Registered the public UMKM pages in the main Website Navbar under the Ekonomi section. - Verified the implementation with `tsc` and `bun run build`. ## Files Created/Modified diff --git a/package.json b/package.json index 93277391..746d8fce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "desa-darmasaba", - "version": "0.1.13", + "version": "0.1.14", "private": true, "scripts": { "dev": "next dev", diff --git a/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts b/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts index 764f5dde..025fee92 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts @@ -122,6 +122,20 @@ export const umkmState = proxy({ } return false; } + }, + findUnique: { + data: null as any, + loading: false, + async load(id: string) { + this.loading = true; + try { + const res = await fetch(`/api/ekonomi/umkm/${id}`); + const result = await res.json(); + if (result.success) { + this.data = result.data; + } + } catch (e) { console.error(e); } finally { this.loading = false; } + } } }, @@ -132,10 +146,17 @@ export const umkmState = proxy({ page: 1, totalPages: 1, loading: false, - async load(page = 1, limit = 10, search = "", umkmId = "") { + async load(page = 1, limit = 10, search = "", umkmId = "", kategoriId = "") { this.loading = true; + this.page = page; try { - const params = new URLSearchParams({ page: page.toString(), limit: limit.toString(), search, umkmId }); + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + search, + umkmId, + kategoriId + }); const res = await fetch(`/api/ekonomi/umkm/produk/find-many?${params}`); const result = await res.json(); if (result.success) { @@ -215,6 +236,24 @@ export const umkmState = proxy({ } }, + // Kategori Produk (Share with Pasar Desa) + kategoriProduk: { + findManyAll: { + data: [] as any[], + loading: false, + async load() { + this.loading = true; + try { + const res = await fetch("/api/ekonomi/pasar-desa/kategori-produk/find-many-all"); + const result = await res.json(); + if (result.success) { + this.data = result.data; + } + } catch (e) { console.error(e); } finally { this.loading = false; } + } + } + }, + // Dashboard Module dashboard: { kpi: { data: null as any, loading: false }, diff --git a/src/app/darmasaba/(pages)/ekonomi/umkm/[id]/page.tsx b/src/app/darmasaba/(pages)/ekonomi/umkm/[id]/page.tsx new file mode 100644 index 00000000..41f64836 --- /dev/null +++ b/src/app/darmasaba/(pages)/ekonomi/umkm/[id]/page.tsx @@ -0,0 +1,120 @@ +'use client' +import umkmState from '@/app/admin/(dashboard)/_state/ekonomi/umkm/umkm'; +import colors from '@/con/colors'; +import { Box, Card, Flex, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Badge, SimpleGrid, Group, Divider, Button } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconBrandWhatsapp, IconMapPinFilled, IconPackage, IconUser } from '@tabler/icons-react'; +import { useParams } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; +import BackButton from '../../../desa/layanan/_com/BackButto'; + +function Page() { + const params = useParams(); + const id = params.id as string; + const state = useProxy(umkmState.umkm.findUnique); + + useShallowEffect(() => { + if (id) state.load(id); + }, [id]); + + if (state.loading || !state.data) { + return ( + + + + ); + } + + const u = state.data; + + return ( + + + + + + + + + {u.nama} + + + + {u.kategori?.nama} + {u.nama} + + + + {u.pemilik} + + + + {u.alamat || 'Desa Darmasaba'} + + + + {u.deskripsi || 'Tidak ada deskripsi tersedia.'} + {u.kontak && ( + + )} + + + + + + + Katalog Produk + + {u.produk?.map((p: any, k: number) => ( + + + {p.nama} + + + {p.nama} + 0 ? 'green' : 'red'}> + {p.stok > 0 ? 'Tersedia' : 'Habis'} + + + + Rp {p.harga.toLocaleString('id-ID')} + + + {p.deskripsi || 'Kualitas terbaik dari UMKM lokal.'} + + + ))} + + {(!u.produk || u.produk.length === 0) && ( +
+ Belum ada produk dari UMKM ini +
+ )} +
+
+ ); +} + +export default Page; diff --git a/src/app/darmasaba/(pages)/ekonomi/umkm/page.tsx b/src/app/darmasaba/(pages)/ekonomi/umkm/page.tsx new file mode 100644 index 00000000..65c55697 --- /dev/null +++ b/src/app/darmasaba/(pages)/ekonomi/umkm/page.tsx @@ -0,0 +1,153 @@ +'use client' +import umkmState from '@/app/admin/(dashboard)/_state/ekonomi/umkm/umkm'; +import colors from '@/con/colors'; +import { Box, Center, Flex, Grid, GridCol, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title, Badge } from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconMapPinFilled, IconSearch, IconUser } from '@tabler/icons-react'; +import { motion } from 'motion/react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import BackButton from '../../desa/layanan/_com/BackButto'; + +function Page() { + const router = useRouter() + const state = useProxy(umkmState.umkm) + const [search, setSearch] = useState(''); + const [debouncedSearch] = useDebouncedValue(search, 1000); + const [selectedCategory, setSelectedCategory] = useState(null); + + const { data, page, loading, totalPages, load } = state.findMany + + useShallowEffect(() => { + umkmState.kategoriProduk.findManyAll.load() + }, []) + + useShallowEffect(() => { + load(page, 8, debouncedSearch, selectedCategory || undefined) + }, [page, debouncedSearch, selectedCategory]) + + if (loading || !data) { + return ( + + + + ) + } + + return ( + + + + + + + + + + Direktori UMKM Desa Darmasaba + + + + setSearch(e.target.value)} + leftSection={} + w={"100%"} + /> + + + + + Daftar Usaha Mikro, Kecil, dan Menengah yang ada di wilayah Desa Darmasaba. + Mendukung pertumbuhan ekonomi lokal melalui pemberdayaan pengusaha desa. + + + + + + + + ({ value: v.id, label: v.nama }))} + value={selectedUmkm} + onChange={setSelectedUmkm} + clearable + searchable + nothingFoundMessage="UMKM tidak ditemukan" + /> +