diff --git a/package.json b/package.json
index dd55fd9..66a3635 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "astro-scaffold",
"type": "module",
- "version": "1.4.0",
+ "version": "1.5.0",
"engines": {
"node": ">=22.12.0"
},
diff --git a/public/work/motion/hut-palu-48.mp4 b/public/work/motion/hut-palu-48-palette.mp4
similarity index 100%
rename from public/work/motion/hut-palu-48.mp4
rename to public/work/motion/hut-palu-48-palette.mp4
diff --git a/public/work/motion/hut-palu-48.webp b/public/work/motion/hut-palu-48-palette.webp
similarity index 100%
rename from public/work/motion/hut-palu-48.webp
rename to public/work/motion/hut-palu-48-palette.webp
diff --git a/public/work/portfolio/ayana/promo.mp4 b/public/work/portfolio/ayana/promo.mp4
new file mode 100644
index 0000000..d75a7ea
Binary files /dev/null and b/public/work/portfolio/ayana/promo.mp4 differ
diff --git a/public/work/portfolio/ayana/promo.webp b/public/work/portfolio/ayana/promo.webp
new file mode 100644
index 0000000..16e0958
Binary files /dev/null and b/public/work/portfolio/ayana/promo.webp differ
diff --git a/public/work/portfolio/cektrans/launch.mp4 b/public/work/portfolio/cektrans/launch.mp4
new file mode 100644
index 0000000..4faa5a2
Binary files /dev/null and b/public/work/portfolio/cektrans/launch.mp4 differ
diff --git a/public/work/portfolio/cektrans/launch.webp b/public/work/portfolio/cektrans/launch.webp
new file mode 100644
index 0000000..12e5eb9
Binary files /dev/null and b/public/work/portfolio/cektrans/launch.webp differ
diff --git a/public/work/portfolio/lumbung/promo.mp4 b/public/work/portfolio/lumbung/promo.mp4
new file mode 100644
index 0000000..ce346ed
Binary files /dev/null and b/public/work/portfolio/lumbung/promo.mp4 differ
diff --git a/public/work/portfolio/lumbung/promo.webp b/public/work/portfolio/lumbung/promo.webp
new file mode 100644
index 0000000..58b4f75
Binary files /dev/null and b/public/work/portfolio/lumbung/promo.webp differ
diff --git a/public/work/portfolio/msp-performance-audit/case-study.mp4 b/public/work/portfolio/msp-performance-audit/case-study.mp4
new file mode 100644
index 0000000..406274b
Binary files /dev/null and b/public/work/portfolio/msp-performance-audit/case-study.mp4 differ
diff --git a/public/work/portfolio/msp-performance-audit/case-study.webp b/public/work/portfolio/msp-performance-audit/case-study.webp
new file mode 100644
index 0000000..2122bed
Binary files /dev/null and b/public/work/portfolio/msp-performance-audit/case-study.webp differ
diff --git a/src/components/cards/CaseStudyCard.astro b/src/components/cards/CaseStudyCard.astro
index 2fd7ad0..a3cf808 100644
--- a/src/components/cards/CaseStudyCard.astro
+++ b/src/components/cards/CaseStudyCard.astro
@@ -10,6 +10,7 @@ import { renderInlineMd } from '../../lib/inline-md'
import { clientLabel } from '../../lib/portfolio-screens'
import Mock from '../mocks/Mock.astro'
import { hasMock } from '../mocks'
+import { hasVideo } from '../../data/portfolio-videos'
interface Props {
title: string
@@ -44,6 +45,7 @@ const bucketLabel = {
}[bucket]
const clientText = clientLabel(client, en ? 'en' : 'id')
const mock = slug ? hasMock(slug) : false
+const video = slug ? hasVideo(slug) : false
const num = index ? String(index).padStart(2, '0') : ''
---
@@ -69,6 +71,7 @@ const num = index ? String(index).padStart(2, '0') : ''
@@ -88,7 +91,7 @@ const num = index ? String(index).padStart(2, '0') : ''
{mock ? : coverSrc && }
-
{bucketLabel} / {clientText} / {year}
+
{bucketLabel} / {clientText} / {year}{video ? ' / Video' : ''}
diff --git a/src/components/mocks/Lumbung.astro b/src/components/mocks/Lumbung.astro
new file mode 100644
index 0000000..1d4225f
--- /dev/null
+++ b/src/components/mocks/Lumbung.astro
@@ -0,0 +1,96 @@
+---
+/**
+ * Port of the Lumbung home ledger (shared workspace) from the Flutter app.
+ * Source: ~/Documents/Projects/Kirana app (HomeLedgerHeader, KPeriodSummaryBar, KLedgerRow, HomeLedgerDaySeparator,
+ * KOfflineBanner, ShellBottomNavBar + ShellFab), via the source-traced rebuild in
+ * education_content_remotion/src/compositions/lumbung-promo/ui (Screen.tsx + screen.css.ts). Cross-checked with the live
+ * mock on https://lumbung.banuacoder.com ("Rumah Budi & Sinta": Income Rp12.400.000, Spending Rp6.553.000).
+ * Tokens (lumbung_ui_kit): bg #FCFCFB, fg #1E293B, fg-subtle #475569, outline-subtle #E2E8F0, primary #146B5C,
+ * income #0B7A57, expense #E02424, harvest #E9A93B, member accents #5A6AC0 / #0891B2.
+ * Layout: design screen 384 wide (dp map 1:1, calc(1 * var(--u)) = 1dp at 384px), status 36, header 12/16/8 padding, ledger row 68, nav 66, FAB 56.
+ * Icons: Harvest set from app/packages/lumbung_asset. People, rows and amounts are sample data (personas, not customers).
+ * Font: the app uses Manrope 800 for amounts and titles; this site does not load Manrope, so the site sans is used.
+ */
+interface Props { locale?: 'id' | 'en' }
+const en = Astro.props.locale === 'en'
+const t = en
+ ? { ws: 'Rumah Budi & Sinta', list: 'List', cal: 'Calendar', inc: 'Income', exp: 'Spending', tot: 'Total', today: 'Today · 16 Jul', yday: 'Yesterday · 15 Jul', off: 'Offline · changes saved locally, will sync.', nav: ['Home', 'Insights', 'Money', 'More'], mon: 'July 2026', food: 'Food', bills: 'Bills' }
+ : { ws: 'Rumah Budi & Sinta', list: 'Daftar', cal: 'Kalender', inc: 'Pemasukan', exp: 'Pengeluaran', tot: 'Total', today: 'Hari ini · 16 Jul', yday: 'Kemarin · 15 Jul', off: 'Offline · perubahan tersimpan lokal, akan disinkronkan.', nav: ['Home', 'Wawasan', 'Uang', 'Lainnya'], mon: 'Juli 2026', food: 'Belanja', bills: 'Tagihan' }
+const I: Record
= {"arrows/chevron-down":" ","navigation/home_active":" ","navigation/insights":" ","navigation/money":" ","navigation/more":" ","navigation/add":" ","finance/groceries-cart":" ","finance/utilities-bolt":" ","filter":" ","left":" ","right":" ","cloudOff":" "}
+const B = ['#5A6AC0', 'B'], S = ['#0891B2', 'S']
+const rows = [
+ { a: 'Rp 186.000', t: 'Superindo', s: 'Sinta · BCA', by: S, c: t.food, col: '#16A34A', tx: '#105E2E', w: '18.42' },
+ { a: 'Rp 95.000', t: 'Listrik PLN', s: 'Budi · BCA', by: B, c: t.bills, col: '#0891B2', tx: '#066880', w: '14.30' },
+ { sep: t.yday, net: '−Rp 128.400' },
+ { a: 'Rp 128.400', t: en ? 'Weekly groceries' : 'Belanja mingguan', s: 'Sinta · GoPay', by: S, c: t.food, col: '#16A34A', tx: '#105E2E', w: '10.05' },
+]
+---
+
+
9.41
+
+
+
{t.ws} B S
+
{t.mon}
+
{t.list} {t.cal}
+
+
{t.inc} Rp12.400.000
{t.exp} Rp6.553.000
{t.tot} +Rp5.847.000
+
{t.off}
+
{t.today} −Rp 281.000
+ {rows.map((r) => r.sep ? (
+
{r.sep} {r.net}
+ ) : (
+
{r.a}
{r.t} {r.by![1]} {r.s}
{r.c} {r.w}
+ ))}
+
+
+
+
diff --git a/src/components/mocks/index.ts b/src/components/mocks/index.ts
index b747435..4634646 100644
--- a/src/components/mocks/index.ts
+++ b/src/components/mocks/index.ts
@@ -10,6 +10,7 @@ import Detexi from './Detexi.astro'
import FormGearBuilder from './FormGearBuilder.astro'
import FormGearEngine from './FormGearEngine.astro'
import Lontara from './Lontara.astro'
+import Lumbung from './Lumbung.astro'
import MspPerformanceAudit from './MspPerformanceAudit.astro'
import Patonro from './Patonro.astro'
import PicoSulteng from './PicoSulteng.astro'
@@ -33,6 +34,7 @@ export const mocks: Record = {
'form-gear-builder': { component: FormGearBuilder, frame: 'desktop', url: 'formgear builder' },
'form-gear-engine': { component: FormGearEngine, frame: 'phone' },
lontara: { component: Lontara, frame: 'phone' },
+ lumbung: { component: Lumbung, frame: 'phone' },
'msp-performance-audit': { component: MspPerformanceAudit, frame: 'phone' },
patonro: { component: Patonro, frame: 'phone' },
'pico-sulteng': { component: PicoSulteng, frame: 'phone' },
diff --git a/src/components/portfolio/CaseStudyDetail.astro b/src/components/portfolio/CaseStudyDetail.astro
index c4b766b..9212462 100644
--- a/src/components/portfolio/CaseStudyDetail.astro
+++ b/src/components/portfolio/CaseStudyDetail.astro
@@ -21,6 +21,8 @@ import CTA from '../sections/CTA.astro'
import DeviceFrame from '../interactive/DeviceFrame.astro'
import Mock from '../mocks/Mock.astro'
import { hasMock } from '../mocks'
+import MotionPlayer from '../services/MotionPlayer.astro'
+import { portfolioVideos } from '../../data/portfolio-videos'
interface Props {
entry: CollectionEntry<'portfolio'>
@@ -40,12 +42,14 @@ const t = en
stack: 'Tech Stack', links: 'Links', linksAria: 'External links', testimonial: 'Testimonial',
next: 'Next project', view: 'View project →', all: 'All projects',
play: 'Get it on Google Play', playImg: '/badges/get-on-google-play.png',
- status: { live: 'Live', maintained: 'Maintained', archived: 'Archived' },
+ status: { live: 'Live', maintained: 'Maintained', archived: 'Archived', development: 'In active development' },
ctaH: 'Need something like this?', ctaSub: 'Tell us about your product or system. We reply with scope, timeline and a realistic estimate.',
screenAlt: (n: number) => `${data.title} screenshot ${n}`,
visit: 'Visit product →',
facts: { origin: 'Origin', client: 'Client', year: 'Year', bucket: 'Category', status: 'Status', stack: 'Stack' },
fig: 'Fig.',
+ video: 'Video', videoSub: 'Watch it in motion. Press play; nothing loads until you do.',
+ labels: { play: 'Play', pause: 'Pause', mute: 'Mute', unmute: 'Unmute', fullscreen: 'Fullscreen', seek: 'Seek' },
}
: {
home: 'Beranda', skip: 'Lewati ke konten utama', summary: 'Ringkasan proyek', detail: 'Detail proyek',
@@ -54,12 +58,14 @@ const t = en
stack: 'Tech Stack', links: 'Tautan', linksAria: 'Tautan eksternal', testimonial: 'Testimoni',
next: 'Proyek berikutnya', view: 'Lihat Proyek →', all: 'Semua proyek',
play: 'Dapatkan di Google Play', playImg: '/badges/dapatkan-di-google-play.png',
- status: { live: 'Live', maintained: 'Dipelihara', archived: 'Diarsipkan' },
+ status: { live: 'Live', maintained: 'Dipelihara', archived: 'Diarsipkan', development: 'Sedang dikembangkan' },
ctaH: 'Butuh sistem seperti ini?', ctaSub: 'Ceritakan produk atau sistem Anda. Kami balas dengan cakupan, jadwal, dan estimasi yang realistis.',
screenAlt: (n: number) => `Tangkapan layar ${data.title} ${n}`,
visit: 'Kunjungi produk →',
facts: { origin: 'Asal', client: 'Klien', year: 'Tahun', bucket: 'Kategori', status: 'Status', stack: 'Stack' },
fig: 'Gbr.',
+ video: 'Video', videoSub: 'Lihat versi bergeraknya. Tekan play; tidak ada yang dimuat sebelum Anda menekannya.',
+ labels: { play: 'Putar', pause: 'Jeda', mute: 'Bisukan', unmute: 'Nyalakan suara', fullscreen: 'Layar penuh', seek: 'Geser posisi video' },
}
const all = (await getCollection('portfolio', ({ data: d }) => !d.draft && d.locale === locale)).sort(
@@ -89,6 +95,7 @@ const bucketLabel = {
}[data.bucket]
const screens = getScreens(data.slug)
+const videos = portfolioVideos[data.slug] ?? []
const phones = screens.filter((s) => s.kind === 'phone')
const desktops = screens.filter((s) => s.kind === 'desktop')
const heroScreen = screens[0]
@@ -252,12 +259,39 @@ const factRow = 'grid grid-cols-[6.5rem_1fr] gap-4 py-3 border-b rule'
+
+ {videos.length > 0 && (
+
+
+
+ № 04 / {t.video}
+ {String(videos.length).padStart(2, '0')} {t.video}
+
+
+
{t.video}
+
{t.videoSub}
+
+
+ {videos.map((vd) => (
+
+
+
+ {vd.kind[locale]}
+ {vd.title[locale]}
+
+
+ ))}
+
+
+
+ )}
+
{screens.length > 0 && (
- № 04 / {t.showcase}
+ № {videos.length > 0 ? '05' : '04'} / {t.showcase}
{String(screens.length).padStart(2, '0')} {t.fig}
{t.showcase}
@@ -299,7 +333,7 @@ const factRow = 'grid grid-cols-[6.5rem_1fr] gap-4 py-3 border-b rule'
-
№ 05 / {t.detail}
+
№ {String(4 + (videos.length > 0 ? 1 : 0) + (screens.length > 0 ? 1 : 0)).padStart(2, '0')} / {t.detail}
diff --git a/src/components/services/MotionPlayer.astro b/src/components/services/MotionPlayer.astro
new file mode 100644
index 0000000..b895228
--- /dev/null
+++ b/src/components/services/MotionPlayer.astro
@@ -0,0 +1,131 @@
+---
+/**
+ * MotionPlayer: branded video player (no native controls). Prospectus language: sharp, hairline,
+ * ultramarine plate with a cyan progress line. Behaviour is in the script below and works for
+ * every [data-mp] on the page, including after ClientRouter navigations.
+ */
+interface Props {
+ src: string
+ poster: string
+ ratio: '9 / 16' | '16 / 9'
+ title: string
+ labels: { play: string; pause: string; mute: string; unmute: string; fullscreen: string; seek: string }
+}
+const { src, poster, ratio, title, labels } = Astro.props
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/services/MotionShowcase.astro b/src/components/services/MotionShowcase.astro
index ac2b8f0..bd438a2 100644
--- a/src/components/services/MotionShowcase.astro
+++ b/src/components/services/MotionShowcase.astro
@@ -3,13 +3,14 @@
* MotionShowcase: real videos made by Banua Coder, shown on the motion-video service page.
* Plain (no autoplay, preload none) so the page stays light. Files live in public/work/motion/.
*/
+import MotionPlayer from './MotionPlayer.astro'
interface Props { locale: 'id' | 'en' }
const { locale } = Astro.props
const en = locale === 'en'
const t = en
- ? { folio: '№ 02 / Showcase', title: 'Our motion work', lede: 'Videos we planned, scripted and rendered in-house with Remotion. Press play to watch.', play: 'Play video' }
- : { folio: '№ 02 / Karya', title: 'Karya video kami', lede: 'Video yang kami rancang, tulis naskahnya, dan render sendiri dengan Remotion. Tekan play untuk menonton.', play: 'Putar video' }
+ ? { folio: '№ 02 / Showcase', title: 'Our motion work', lede: 'Videos we planned, scripted and rendered in-house with Remotion. Press play to watch.', play: 'Play video', labels: { play: 'Play', pause: 'Pause', mute: 'Mute', unmute: 'Unmute', fullscreen: 'Fullscreen', seek: 'Seek' } }
+ : { folio: '№ 02 / Karya', title: 'Karya video kami', lede: 'Video yang kami rancang, tulis naskahnya, dan render sendiri dengan Remotion. Tekan play untuk menonton.', play: 'Putar video', labels: { play: 'Putar', pause: 'Jeda', mute: 'Bisukan', unmute: 'Nyalakan suara', fullscreen: 'Layar penuh', seek: 'Geser posisi video' } }
const items = [
{
@@ -25,7 +26,7 @@ const items = [
note: en ? 'How we found and fixed a jank problem, told as a short explainer.' : 'Cara kami menemukan dan memperbaiki masalah jank, dikemas sebagai penjelasan singkat.',
},
{
- src: '/work/motion/hut-palu-48.mp4', poster: '/work/motion/hut-palu-48.webp', ratio: '9 / 16',
+ src: '/work/motion/hut-palu-48-palette.mp4', poster: '/work/motion/hut-palu-48-palette.webp', ratio: '9 / 16',
kind: en ? 'City anniversary · 9:16 · 29 s' : 'Ucapan HUT kota · 9:16 · 29 dtk',
title: en ? '48th anniversary of Palu' : 'HUT ke-48 Kota Palu',
note: en ? 'A greeting reel for the city of Palu, showing our work for Palu and Central Sulawesi.' : 'Reel ucapan untuk Kota Palu, menampilkan karya kami untuk Palu dan Sulawesi Tengah.',
@@ -49,13 +50,7 @@ const items = [
{items.map((v) => (
-
-
-
+
{v.kind}
{v.title}
diff --git a/src/content.config.ts b/src/content.config.ts
index 90386a6..5d16364 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -23,7 +23,7 @@ const portfolio = defineCollection({
client: z.string(),
clientUrl: z.string().url().optional(),
year: z.number().int(),
- status: z.enum(['live', 'maintained', 'archived']),
+ status: z.enum(['live', 'maintained', 'archived', 'development']),
draft: z.boolean().default(false),
cover: image(),
thumbnail: image(),
diff --git a/src/content/portfolio/lumbung/cover.webp b/src/content/portfolio/lumbung/cover.webp
new file mode 100644
index 0000000..74f363b
Binary files /dev/null and b/src/content/portfolio/lumbung/cover.webp differ
diff --git a/src/content/portfolio/lumbung/index.en.mdx b/src/content/portfolio/lumbung/index.en.mdx
new file mode 100644
index 0000000..cc98502
--- /dev/null
+++ b/src/content/portfolio/lumbung/index.en.mdx
@@ -0,0 +1,99 @@
+---
+title: Lumbung
+slug: lumbung
+bucket: consumer
+client: Banua Coder
+clientUrl: https://lumbung.banuacoder.com
+year: 2026
+status: development
+cover: ./cover.webp
+thumbnail: ./thumbnail.webp
+summary: >
+ A personal and shared finance app that works fully without a signal. Lumbung
+ pairs the logging speed of a money manager with shared spaces for couples,
+ families, housemates and groups. Our own product, in active development and
+ currently taking waitlist sign-ups.
+problem: >
+ Money is rarely managed alone. Couples split the household bills, families
+ keep one budget, housemates chip in for electricity. Most trackers are built
+ for one person, or need the internet before they can be shared. An entry
+ written somewhere with no signal must not be lost either. And financial data
+ is sensitive: people need to know it is encrypted and can be exported at any
+ time.
+solution: >
+ Banua Coder is building Lumbung as an offline-first finance platform. The
+ Flutter client writes every entry to a local database first, then syncs it
+ per entry across devices when the signal returns. One Workspace concept
+ holds both personal and shared ledgers, with roles (owner, admin, member,
+ viewer), and every entry shows who added it. A Go backend handles sync,
+ accounts and subscriptions, while the marketing site and admin console are
+ built separately.
+impact:
+ - "In active, open development: the site is live with a waitlist, the app is not released yet"
+ - "Core work done so far: offline-first logging, cross-device sync, and shared spaces with roles"
+ - "Features such as split bills, budgets and import from an old tracker are still on the roadmap and marked that way on the site"
+ - "One product, three parts we build ourselves: the Flutter app, the Go backend and the marketing site"
+stack:
+ - Flutter
+ - Dart
+ - Bloc
+ - Melos
+ - Go
+ - Echo
+ - PostgreSQL
+ - River
+ - Nuxt 4
+ - Tailwind v4
+links:
+ web: https://lumbung.banuacoder.com
+featured: true
+order: 10
+locale: en
+---
+
+## Context
+
+A lumbung is the Indonesian communal rice granary raised on stilts, where a
+household or village stores its harvest together. The name fits because the
+product is about storing together: one place for personal money, money shared
+by two, and each person's share of it. It is a working name and not final.
+
+This is our own product, not client work. It is in active development: the
+site is live and takes waitlist sign-ups, but the app cannot be downloaded yet.
+This page only describes what exists and marks what does not.
+
+## What we are building
+
+- **Flutter client**: a Melos workspace with clean architecture and
+ Bloc/Cubit. Every entry is written to an encrypted local database first, so
+ the app stays fully usable with no network.
+- **Shared spaces**: one Workspace concept for personal, couple, family,
+ housemate, group trip and small business use. People are invited with a
+ role, not by sharing a password.
+- **Sync**: changes sync per entry across devices with automatic conflict
+ merge, not as a single whole-file backup.
+- **Precise money**: amounts are stored as minor units, a currency code and a
+ scale, so rupiah, yen and Kuwaiti dinar are not forced into one two-decimal
+ rule.
+- **Go backend**: a modular monolith with Echo, pgx, River for background
+ jobs, and PostgreSQL. It handles sync, accounts and billing.
+- **Marketing site and admin console**: a bilingual site with a waitlist
+ (Nuxt 4, Tailwind v4), plus an admin console and a status page.
+
+## Engineering notes
+
+Offline-first here is not a fallback mode. The app writes to local storage
+first and the network only delivers the result. That means sync has to merge
+two devices that both changed data while disconnected, and that is where most
+of the backend work goes.
+
+The other principles we hold: calm language, with no gamification and no
+guilt; data encrypted on the device; no scraping of users' bank accounts; and
+data that can be exported at any time.
+
+## Where it stands
+
+Lumbung is not released and has no users yet. That is why this page carries no
+user numbers, testimonials or app store links, and why the prices on the site
+are still plans. Follow progress on the
+[Lumbung site](https://lumbung.banuacoder.com).
diff --git a/src/content/portfolio/lumbung/index.id.mdx b/src/content/portfolio/lumbung/index.id.mdx
new file mode 100644
index 0000000..4502eda
--- /dev/null
+++ b/src/content/portfolio/lumbung/index.id.mdx
@@ -0,0 +1,103 @@
+---
+title: Lumbung
+slug: lumbung
+bucket: consumer
+client: Banua Coder
+clientUrl: https://lumbung.banuacoder.com
+year: 2026
+status: development
+cover: ./cover.webp
+thumbnail: ./thumbnail.webp
+summary: >
+ Aplikasi keuangan pribadi dan bersama yang bekerja penuh tanpa sinyal.
+ Lumbung memadukan pencatatan secepat aplikasi money manager dengan ruang
+ bersama untuk pasangan, keluarga, teman serumah, dan kelompok. Produk kami
+ sendiri, sedang dikembangkan dan baru membuka daftar tunggu.
+problem: >
+ Uang jarang dikelola sendirian. Pasangan membagi tagihan rumah, keluarga
+ menjaga satu anggaran, teman serumah patungan listrik. Aplikasi pencatat yang
+ ada biasanya hanya untuk satu orang, atau butuh internet agar bisa dipakai
+ bersama. Catatan yang ditulis di tempat tanpa sinyal pun tidak boleh hilang.
+ Di sisi lain, data keuangan itu sensitif: pengguna perlu tahu datanya
+ terenkripsi dan bisa diekspor kapan saja.
+solution: >
+ Banua Coder membangun Lumbung sebagai platform keuangan offline-first. Klien
+ Flutter menulis setiap catatan lebih dulu ke basis data lokal, lalu
+ menyinkronkannya per entri antarperangkat saat sinyal kembali. Satu konsep
+ Workspace mewadahi buku kas pribadi maupun bersama, lengkap dengan peran
+ (pemilik, admin, anggota, pengamat), dan setiap catatan menunjukkan siapa yang
+ menambahkannya. Backend Go bertugas di sisi sinkronisasi, akun, dan
+ langganan, sementara situs pemasaran dan panel admin dibangun terpisah.
+impact:
+ - "Sedang dikembangkan secara terbuka: situs sudah tayang dengan daftar tunggu, aplikasi belum dirilis"
+ - "Inti yang sudah dikerjakan: pencatatan offline-first, sinkronisasi antarperangkat, dan ruang bersama berperan"
+ - "Fitur seperti patungan, anggaran, dan impor dari aplikasi lama masih di peta jalan dan ditandai demikian di situs"
+ - "Satu produk, tiga bagian yang kami bangun sendiri: aplikasi Flutter, backend Go, dan situs pemasaran"
+stack:
+ - Flutter
+ - Dart
+ - Bloc
+ - Melos
+ - Go
+ - Echo
+ - PostgreSQL
+ - River
+ - Nuxt 4
+ - Tailwind v4
+links:
+ web: https://lumbung.banuacoder.com
+featured: true
+order: 10
+locale: id
+---
+
+## Konteks
+
+Lumbung adalah kata Indonesia untuk gudang padi bersama yang didirikan di atas
+tiang, tempat satu rumah tangga atau kampung menyimpan hasil panennya. Nama itu
+dipilih karena produknya juga soal menyimpan bersama: satu tempat untuk uang
+pribadi, uang berdua, dan bagian masing-masing orang di dalamnya. Nama ini
+masih nama kerja, belum final.
+
+Ini produk kami sendiri, bukan pekerjaan klien. Statusnya sedang dikembangkan:
+situs sudah tayang dan menerima pendaftaran daftar tunggu, tetapi aplikasinya
+belum bisa diunduh. Halaman ini hanya memuat apa yang sudah ada dan memberi
+tanda pada yang belum.
+
+## Yang kami bangun
+
+- **Klien Flutter**: workspace Melos dengan arsitektur bersih dan Bloc/Cubit.
+ Setiap catatan ditulis ke basis data lokal terenkripsi lebih dulu, sehingga
+ aplikasi tetap bisa dipakai penuh tanpa jaringan.
+- **Ruang bersama**: satu konsep Workspace untuk pribadi, pasangan, keluarga,
+ teman serumah, perjalanan kelompok, dan usaha kecil. Anggota diundang dengan
+ peran, bukan dengan berbagi kata sandi.
+- **Sinkronisasi**: perubahan disinkronkan per entri antarperangkat dengan
+ penggabungan konflik otomatis, bukan cadangan satu berkas utuh.
+- **Uang yang presisi**: nominal disimpan sebagai jumlah satuan terkecil,
+ kode mata uang, dan skala, jadi rupiah, yen, dan dinar Kuwait tidak dipaksa
+ memakai dua desimal yang sama.
+- **Backend Go**: modular monolith dengan Echo, pgx, River untuk pekerjaan
+ latar belakang, dan PostgreSQL. Ia menangani sinkronisasi, akun, dan
+ penagihan.
+- **Situs pemasaran dan panel admin**: situs dwibahasa dengan daftar tunggu
+ (Nuxt 4, Tailwind v4), ditambah panel admin dan halaman status.
+
+## Catatan teknis
+
+Offline-first di sini bukan mode cadangan. Aplikasi menulis ke penyimpanan
+lokal lebih dulu, jaringan hanya mengantarkan hasilnya. Konsekuensinya,
+sinkronisasi harus bisa menggabungkan dua perangkat yang sama-sama mengubah
+data saat terputus, dan itulah bagian yang paling banyak dikerjakan di
+backend.
+
+Prinsip lain yang kami pegang: bahasa yang tenang, tanpa gamifikasi dan tanpa
+nada menyalahkan; data terenkripsi di perangkat; tidak memindai rekening bank
+pengguna; dan data bisa diekspor kapan saja.
+
+## Status saat ini
+
+Lumbung belum dirilis dan belum punya pengguna. Karena itu halaman ini tidak
+memuat angka pengguna, testimoni, atau tautan toko aplikasi. Harga di situs
+juga masih rencana. Perkembangan terbaru bisa dilihat di
+[situs Lumbung](https://lumbung.banuacoder.com).
diff --git a/src/content/portfolio/lumbung/thumbnail.webp b/src/content/portfolio/lumbung/thumbnail.webp
new file mode 100644
index 0000000..7e03f1e
Binary files /dev/null and b/src/content/portfolio/lumbung/thumbnail.webp differ
diff --git a/src/data/portfolio-videos.ts b/src/data/portfolio-videos.ts
new file mode 100644
index 0000000..63a8ab3
--- /dev/null
+++ b/src/data/portfolio-videos.ts
@@ -0,0 +1,17 @@
+/** Portfolio slug -> case-study videos (files in public/work/portfolio//, compressed, lazy, no autoplay). */
+interface L { id: string; en: string }
+export interface PortfolioVideo { src: string; poster: string; ratio: '9 / 16' | '16 / 9'; title: L; kind: L }
+
+const v = (slug: string, file: string, ratio: PortfolioVideo['ratio'], title: L, kind: L): PortfolioVideo => ({
+ src: `/work/portfolio/${slug}/${file}.mp4`, poster: `/work/portfolio/${slug}/${file}.webp`, ratio, title, kind,
+})
+
+export const portfolioVideos: Record = {
+ ayana: [v('ayana', 'promo', '16 / 9', { id: 'Video promo Ayana', en: 'Ayana promo film' }, { id: 'Promo produk · 16:9 · 2 mnt 58 dtk', en: 'Product promo · 16:9 · 2 min 58 s' })],
+ cektrans: [v('cektrans', 'launch', '16 / 9', { id: 'Video peluncuran cektrans', en: 'cektrans launch film' }, { id: 'Peluncuran produk · 16:9 · 61 dtk', en: 'Product launch · 16:9 · 61 s' })],
+ lumbung: [v('lumbung', 'promo', '9 / 16', { id: 'Video promo Lumbung', en: 'Lumbung promo film' }, { id: 'Promo produk · 9:16 · 72 dtk', en: 'Product promo · 9:16 · 72 s' })],
+ // The case-study body already embeds 5 before/after clips; this is the separate 9:16 explainer (not one of them).
+ 'msp-performance-audit': [v('msp-performance-audit', 'case-study', '9 / 16', { id: 'Audit performa Flutter, ringkasan video', en: 'Flutter performance audit, video summary' }, { id: 'Studi kasus · 9:16 · 88 dtk', en: 'Case study · 9:16 · 88 s' })],
+}
+
+export const hasVideo = (slug: string) => slug in portfolioVideos