Portfolio bergaya Persona 3 Reload — port dari Persona-Quickshell (QML) ke browser.
Tanpa router, tanpa animation library, tanpa CSS framework. Semua panel dibaca ulang dari QML sumbernya, bukan dijiplak dari screenshot.
Repo ini isinya cuma tulisan ini. Kodenya privat — kalau kamu perlu lihat, tinggal bilang.
Dokumentasi teknis portfolio ini tidak ditulis sebagai daftar. Dia ditaruh di dalam menara.
Cara main: klik
▸untuk masuk ruangan. Tiap lantai dijaga satu Shadow. Pilih satu serangan — kalau salah kamu kena Repel (dan tetap dapat penjelasan kenapa salah), kalau benar dapat ONE MORE! dan lantainya menjatuhkan dokumentasi aslinya sebagai loot.Empat lantai bersih → The Reaper terbuka. Tidak mau main? Ada save point di bawah.
ENTRANCE ── 1F THEBEL ── 2F ARQA ── 3F YABBASHAH ── 4F ADAMAH ── ☠ MONAD
🔵 1F · THEBEL — The Server Vault guardian: Cachebound Table
Ruangan sempit, dinding penuh header HTTP yang membeku. Sesuatu berbentuk meja mengambang, halamannya berdetak seperti jantung.
Cachebound Table menghalangi jalan. Dia hanya bisa dijatuhkan oleh strategi cache yang benar.
HP ▓▓▓▓▓▓▓▓▓▓ | Arcana: The Hermit | Weak: ?
Pilih serangan:
🔥 Agi — semua respons pakai no-cache, aman
MISS. Aman, tapi mahal. Font, favicon, dan bundle ber-hash ikut divalidasi ulang tiap load — padahal tiga-tiganya tidak pernah berubah tanpa ganti nama file. Kamu buang round trip untuk byte yang sudah ada di disk user.
Cachebound Table masih berdiri. Coba lagi.
❄️ Bufu — semua di /assets/ dapat immutable
REPEL — kamu kena 40 damage sendiri.
Ini persis bug yang pernah hidup di repo ini. /assets/ tidak hanya berisi file ber-hash Vite; ada juga gambar dan audio yang di-copy apa adanya dari public/. Nama file mereka tidak berubah saat isinya diganti, jadi max-age=31536000, immutable mengunci versi lama di browser user selama setahun dan tidak ada cara memaksa refresh selain rename.
Cachebound Table menyeringai. Coba lagi.
⚡ Zio — immutable hanya untuk nama file ber-hash + folder font
Cachebound Table hancur. Loot jatuh:
var hashedBundle = regexp.MustCompile(`-[A-Za-z0-9_-]{8,}\.[A-Za-z0-9]+$`)
// Nama file font sudah membawa family + subset-nya, jadi revisi berarti nama baru,
// bukan byte baru di bawah nama lama — jaminan yang sama dengan hash Vite.
func immutableAsset(urlPath string) bool {
if strings.HasPrefix(urlPath, "/assets/fonts/") {
return true
}
return strings.HasPrefix(urlPath, "/assets/") && hashedBundle.MatchString(path.Base(urlPath))
}Jalur request (cmd/server/main.go, 116 baris):
flowchart LR
R([request]) --> A{prefix /api/ ?}
A -- ya --> API[API handler]
A -- tidak --> D{DEV_PROXY diisi?}
D -- ya --> V[proxy ke Vite :5173]
D -- tidak --> F{file ada di DIST_DIR?}
F -- ya --> C{immutableAsset ?}
C -- ya --> I[max-age=31536000, immutable]
C -- tidak --> N[no-cache]
F -- tidak --> S[fallback index.html<br/>route client-side selamat saat hard refresh]
Environment:
| Var | Default | Keterangan |
|---|---|---|
ADDR |
:8080 |
Alamat listen |
DIST_DIR |
web/dist |
Folder hasil build |
DEV_PROXY |
— | Kalau diisi, semua non-API di-proxy ke sini |
1F THEBELCLEARED ✓
🌪 Garu — matikan caching, biar selalu fresh
NULL. Shadow tidak bergeming. "Selalu fresh" itu bukan strategi cache, itu menyerah — dan tetap tidak menjamin apa-apa, karena tanpa header eksplisit browser boleh menebak sendiri lewat heuristik.
Coba lagi.
🟦 2F · ARQA — The Component Wing guardian: Maya of Many Renders
Lorong bercabang jadi 21 pintu, tiap pintu satu komponen. Di tengahnya sesuatu terus me-mount dan un-mount dirinya sendiri, dan tiap kali itu terjadi musik di ruangan ini mengulang dari awal.
HP ▓▓▓▓▓▓▓▓▓▓ | Arcana: The Magician | Weak: ?
Musiknya restart tiap kali pindah halaman. Kenapa?
🔥 Agi — file audionya kekecilan, tinggal di-loop
MISS. Loop mengurus akhir track, bukan awalnya. Yang terjadi di sini adalah elemen audionya sendiri lenyap lalu dibuat ulang — posisi playback ikut hilang bersamanya.
Coba lagi.
⚡ Zio — elemen <audio> hidup di dalam Shell, dan Shell ikut unmount saat keluar desktop
Benar. Kepemilikan state-nya dinaikkan ke App, di atas percabangan route:
/** Owned here rather than in Shell: leaving the desktop unmounts Shell, and with it
the audio element and its graph, so the track would restart on every route. */
const TRACK_SRC = '/assets/audio/theme.m4a'Maya of Many Renders pecah. Peta lantainya terbuka:
cmd/server/main.go Go net/http — API, static, SPA fallback, cache policy
web/
index.html @font-face di-inline + preload scene/font
src/
App.tsx route switch, hotkey, kepemilikan audio
routes.ts ROUTES + MENU (urutan 01–04 dikunci di sini)
views/
Shell.tsx desktop shell
MobileShell.tsx layout mobile — desain sendiri, bukan desktop yang dikecilkan
Page.tsx kerangka halaman dalam
content.tsx isi Projects / Skills / About / Contact
components/ 21 komponen (tabel di bawah)
hooks/ useMediaQuery · usePersonaTime · useSystemInfo
useTrackSpectrum · useTransitionRoute
lib/ cava · rain · system · time
styles/tokens.css satu-satunya sumber warna, easing, tilt, type scale
data/projects.ts 9 project + tanggal untuk timeline & kalender
| Komponen | Perannya |
|---|---|
Wallpaper |
5 layer scene — langit, hujan di canvas, bar bergerak, cava, kaca depan |
BarWipe / SlabWipe / BubbleWipe |
Transisi antar route, port dari P3rTransition.qml |
OptionsBars / OptionsPanel |
Kolom bar hitam + panel miring saat item dipilih |
TiltMenu / Marquee / TextMarquee |
Menu miring dan teks berjalan |
Clock / Capsule / CavaVisualizer |
Jam, kapsul musik, dan spektrum audio-nya |
CalendarOverlay |
Tangga 7 hari, bisa digeser harian, lompat ke tanggal project |
Timeline |
Mode tampilan kedua di /projects |
ProjectDetail / Lightbox |
Halaman detail + galeri per project |
CommandPalette |
⌘K atau / — semua route dan link |
EdgeDock / ResumeList / ResumePanel / SkewPanel |
Dock tepi dan panel-panel miring |
2F ARQACLEARED ✓
❄️ Bufu — React 19 memang begitu, tidak bisa diapa-apakan
NULL. Shadow tertawa. React tidak pernah menghancurkan apa pun tanpa alasan struktural — kalau sesuatu ikut hilang, dia memang dipasang di cabang pohon yang ikut dilepas. Naikkan kepemilikannya.
Coba lagi.
🟨 3F · YABBASHAH — The Foundry guardian: Blocking Wraith
Lantai ini terang tapi kosong: teks belum muncul, gambar belum turun. Semuanya sedang antre di belakang satu permintaan ke domain lain.
HP ▓▓▓▓▓▓▓▓▓▓ | Arcana: The Hanged Man | Weak: ?
Font datang dari Google Fonts dan menahan render pertama. Serang:
🌪 Garu — tambah preconnect ke dua origin Google
MISS — 12 damage. Ini memperbaiki gejalanya, bukan bentuknya. Rantainya tetap: DNS + TLS ke fonts.googleapis.com → CSS → DNS + TLS ke fonts.gstatic.com → woff2. preconnect cuma memotong bagian awalnya; jumlah hop-nya tidak berkurang.
Coba lagi.
🔥 Agi — self-host woff2, inline @font-face ke index.html, preload font + scene
Blocking Wraith menguap. Rantai lintas-origin-nya hilang seluruhnya — dua preconnect dan satu <link> stylesheet dihapus, diganti tiga woff2 lokal (±54 KB) dengan @font-face yang ditanam langsung di <head>.
Dua temuan yang mahal didapat, ditinggalkan di sini:
- Montserrat itu variable font. Ketahuan karena CSS Google menyajikan URL woff2 latin yang sama persis untuk weight 300/400/600/700. Satu file menutup seluruh rentang:
font-weight: 300 700.grep -a fvarpada woff2 tidak bisa dipakai untuk membuktikan ini — tabelnya dikompres Brotli, jadi hasilnya negatif palsu. Verifikasinya di runtime: lebar teks 529.36 / 535.17 / 558.63 px pada weight 300/400/700 membuktikan axis-nya hidup. - Gambar yang di-mount React tidak bisa mulai di-fetch sebelum bundle-nya di-parse.
<link rel="preload" as="image">di<head>memindahkan fetch-nya ke waktu parse HTML.
Hasil ukur (bukan estimasi):
| Sebelum | Sesudah | |
|---|---|---|
Request ke fonts.g* |
3 + 2 preconnect | 0 |
| Font siap | setelah rantai lintas-origin | 46 / 51 / 60 ms |
| Scene mulai turun | setelah bundle di-parse | 12 ms |
| Cache font | pihak ketiga | immutable, 1 tahun |
Ukuran bundle produksi:
| Berkas | Mentah | Gzip |
|---|---|---|
index.html |
3.63 kB | — |
| CSS | 53.74 kB | 11.52 kB |
| JS | 246.52 kB | 78.08 kB |
Angka JS itu sudah termasuk React + React DOM. Sisanya — router, transisi, kalender, palette, visualizer — ditulis tangan, jadi tidak ada yang menumpang di atasnya.
3F YABBASHAHCLEARED ✓
⚡ Zio — pakai font-display: block biar tidak ada teks yang loncat
REPEL. Kamu menukar layout shift dengan halaman kosong. block menahan teks sampai font turun; yang dipakai di sini swap, supaya teks langsung terbaca dan fontnya menyusul. Lagipula ini masih tidak menyentuh akar masalahnya: request-nya masih ke domain orang lain.
Coba lagi.
🔴 4F · ADAMAH — Chamber of Tokens guardian: The Skewed Emperor
Ruangan terakhir sebelum Monad. Semua yang ada di sini miring −20°, dan huruf-huruf di dindingnya terpotong di ujung atas dan bawah.
HP ▓▓▓▓▓▓▓▓▓▓ | Arcana: The Emperor | Weak: ?
Teks yang diputar −30° di dalam bar hitam huruf pertama dan terakhirnya tergunting. Kenapa?
❄️ Bufu — teksnya terlalu panjang, meluber ke kanan
MISS. Ini dugaan pertama yang salah waktu bug-nya benar-benar diburu. Probe geometri mengembalikan clippedPx negatif di semua lebar viewport — artinya tidak pernah ada overflow horizontal sama sekali. Arah potongannya bukan yang itu.
Coba lagi.
🌪 Garu — rotate tidak mengubah kotak layout; jejak vertikal kata jadi ±1.8× font-size-nya
Tepat. Kata yang diputar −30° menempati W·sin30 + H·cos30 secara vertikal ≈ 1.8× font-size-nya. Karena rotate itu transform, layout tidak tahu apa-apa — jadi overflow: hidden milik bar menggunting huruf pertama dan terakhir tanpa satu pun peringatan layout. Terukur: tinggi hasil rotasi 65px di dalam bar 52px, lewat 7px di atas dan 7px di bawah.
Perbaikannya bukan angka ajaib, tapi batas kedua terhadap tinggi bar yang jadi wadahnya:
font-size: min(
clamp(26px, calc(var(--obar-w) * 0.087), 50px),
calc(var(--obar-h) * 0.5)
);The Skewed Emperor runtuh dan menjatuhkan tokens.css:
| Token | Nilai | Asal |
|---|---|---|
--c-base · --c-base-deep |
#0d1a3a · #08153f |
Data/Colors.qml (Pywal16) |
--c-blue · --c-cyan |
#1a6aff · #7dd4fc |
idem |
--c-yellow · --c-red |
#ffe700 · #c4001a |
idem |
--e-out-back |
cubic-bezier(0.34, 1.56, 0.64, 1) |
Qt OutBack(1.7) |
--e-out-back-hard |
cubic-bezier(0.34, 1.9, 0.64, 1) |
Qt OutBack(3.0) |
--e-in-out-quart |
cubic-bezier(0.77, 0, 0.175, 1) |
Qt InOutQuart |
--e-out-expo |
cubic-bezier(0.16, 1, 0.3, 1) |
Qt OutExpo |
--t-wipe |
350ms |
durasi satu slab P3rTransition.qml |
Catatan port lain:
- Panel miring
-20degdengan garis merah 8px di atas —Layers/OptionsList.qml. - Teks di dalam elemen miring selalu di-counter-rotate supaya tetap terbaca datar —
Layers/Resume.qml. BarWipe= 3 slab, delay 0/100/200ms, tiap slab 350ms masuk → tahan 200ms → 350ms keluar.- Di bawah
prefers-reduced-motion: reduce, semua durasi kolaps ke1ms.
4F ADAMAHCLEARED ✓
🔥 Agi — hapus overflow: hidden dari bar-nya
REPEL — 25 damage. Sekarang hurufnya utuh, tapi bocor keluar bar dan menimpa potret karakter di sebelahnya. overflow: hidden itu bagian dari desainnya, bukan penyebabnya.
Coba lagi.
☠️ MONAD — The Reaper hanya terbuka setelah 4 lantai bersih
Rantai terseret di lantai. Kamu mendengarnya sebelum melihatnya.
The Reaper tidak menanyakan apa pun. Dia cuma bertanya apakah build-mu jalan.
cd web && pnpm build # → web/dist
go build -o persona-server ./cmd/server
./persona-server # sajikan dist, fallback index.htmlTanpa DEV_PROXY, server menyajikan DIST_DIR dengan fallback index.html supaya route client-side tetap hidup saat hard refresh.
Status akhir:
| TypeScript | 3.331 baris, strict, tsc --noEmit bersih |
| CSS | 3.826 baris, tanpa framework |
| Go | 116 baris, hanya net/http |
| Dependency runtime | react, react-dom — habis |
| Project di katalog | 9 |
MONADCLEARED ✓ — menara tamat.
Empat entri menu shell. Urutannya dikunci di web/src/routes.ts, jadi kalender dan command palette tidak pernah berbeda urutan dari bar-nya.
| # | Route | Role | Sebagai |
|---|---|---|---|
01 |
/projects |
WORK |
Product yang sudah rilis, side quest, dan yang selamat sampai produksi |
02 |
/skills |
STACK |
Stack yang dipakai, dan sedalam apa masing-masing |
03 |
/about |
SELF |
Asal-usul, alasan membangun, arah berikutnya |
04 |
/contact |
LINK |
Mail, GitHub, LinkedIn |
Buat yang tidak mau masuk menara — versi tiga barisnya:
Portfolio pribadi bertema Persona 3 Reload, di-port dari shell desktop QML ke browser. Server Go 116 baris, frontend React 19 tanpa satu pun dependency UI — router, transisi, kalender, command palette, dan visualizer audionya ditulis tangan semua. Layout mobile-nya desain terpisah, bukan desktop yang dipaksa masuk media query.
Pintasan di dalam aplikasi: ⌘K / Ctrl+K / / membuka command palette.
Repo ini sengaja cuma berisi tulisan. Source-nya privat — kalau kamu butuh baca kodenya (recruiter, review, atau sekadar penasaran), kontak saya dan saya kasih akses.
Kode portfolio-nya tulisan sendiri. Aset visualnya tidak semuanya.
- Layout, komponen, transisi, dan tipografi dibangun ulang sebagai CSS — bukan gambar. Yang diambil dari QML sumber adalah nilainya (warna, easing, sudut, durasi), bukan berkasnya.
- Potret karakter dan beberapa elemen scene turunan dari media Persona 3 Reload — © ATLUS / SEGA. Dipakai untuk portfolio pribadi non-komersial, tidak dilisensikan untuk didistribusikan ulang.
- Repo sumber (Persona-Quickshell) menyebut MIT di README-nya, tapi tidak ada berkas
LICENSEdi tree-nya — jadi anggap kodenya MIT dan asetnya tidak.
Ringkasnya: silakan pinjam tekniknya, jangan pinjam gambarnya.
ANAPHYGON · anaphygon.my.id