// ============================= // Vite + React + Tailwind App // Dashboard functional + pages wired to n8n // Files are concatenated here. When implementing, split into the paths shown. // ============================= // ------------------------------------------ // File: src/main.jsx // ------------------------------------------ import React from 'react' import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import App from './App' import './index.css' import { AuthProvider } from './lib/auth' ReactDOM.createRoot(document.getElementById('root')).render( ) // ------------------------------------------ // File: src/App.jsx // ------------------------------------------ import React from 'react' import { Routes, Route, Navigate } from 'react-router-dom' import Layout from './components/Layout' import DashboardPage from './pages/DashboardPage' import CreateStoryPage from './pages/CreateStoryPage' import UploadPage from './pages/UploadPage' import DownloadsPage from './pages/DownloadsPage' import PrintCheckoutPage from './pages/PrintCheckoutPage' import OrdersPage from './pages/OrdersPage' import StoriesPage from './pages/StoriesPage' import DonationsPage from './pages/DonationsPage' import TopUpPage from './pages/TopUpPage' import LoginPage from './pages/LoginPage' import { useAuth } from './lib/auth' function ProtectedRoute({ children }) { const { user } = useAuth() if (!user) return return children } export default function App(){ return ( } /> } > } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ) } // ------------------------------------------ // File: src/lib/auth.jsx (very simple client auth state) // ------------------------------------------ import React, { createContext, useContext, useEffect, useState } from 'react' const AuthCtx = createContext(null) export function AuthProvider({ children }){ const [user, setUser] = useState(null) useEffect(()=>{ const saved = localStorage.getItem('bookoomoo:user') if(saved) setUser(JSON.parse(saved)) },[]) const login = (payload)=>{ setUser(payload); localStorage.setItem('bookoomoo:user', JSON.stringify(payload)) } const logout = ()=>{ setUser(null); localStorage.removeItem('bookoomoo:user') } return {children} } export function useAuth(){ return useContext(AuthCtx) } // ------------------------------------------ // File: src/lib/api.js (n8n endpoints wrapper) // ------------------------------------------ export const API = { // Set your n8n base here (domain/webhook path). For local dev, use proxy. BASE_URL: import.meta.env.VITE_N8N_BASE_URL || 'http://localhost:5678', async get(path){ const res = await fetch(`${this.BASE_URL}${path}`) if(!res.ok) throw new Error('Request failed') return res.json() }, async post(path, body){ const res = await fetch(`${this.BASE_URL}${path}`,{ method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) }) if(!res.ok) throw new Error('Request failed') return res.json() }, // ---- Bookoomoo specific (align with PRD) ---- me(){ return this.get('/webhook/user/me') }, dashboard(){ return this.get('/webhook/dashboard/summary') }, listStories(){ return this.get('/webhook/stories/list') }, listOrders(){ return this.get('/webhook/orders/list') }, listDonations(){ return this.get('/webhook/donations/list') }, topupToken(data){ return this.post('/webhook/token/topup', data) }, // Story flow genStory(payload){ return this.post('/webhook/story/generate', payload) }, genPDF(payload){ return this.post('/webhook/pdf/generate', payload) }, createPrintOrder(payload){ return this.post('/webhook/print/order', payload) }, } // ------------------------------------------ // File: src/components/Layout.jsx // ------------------------------------------ import React from 'react' import { Outlet, Link, useNavigate } from 'react-router-dom' import { BookOpen, Gift, Home, LogOut, Printer, Search, Upload, Wallet, Download, PlusCircle } from 'lucide-react' import { useAuth } from '../lib/auth' function Shell(){ const { logout } = useAuth() const nav = useNavigate() return (
📚
Bookoomoo
© {new Date().getFullYear()} Bookoomoo — Be Different, Be You.
) } export default function Layout(){ return } // ------------------------------------------ // File: src/components/StatusBadge.jsx // ------------------------------------------ import React from 'react' export default function StatusBadge({ status }){ const map = { Draft: 'bg-neutral-100 text-neutral-700', 'PDF Ready': 'bg-blue-100 text-blue-700', Printed: 'bg-emerald-100 text-emerald-700', Completed: 'bg-emerald-100 text-emerald-700', 'In Production': 'bg-amber-100 text-amber-700', Delivered: 'bg-purple-100 text-purple-700', } const cls = map[status] || 'bg-neutral-100 text-neutral-700' return {status} } // ------------------------------------------ // File: src/pages/DashboardPage.jsx // (Fetches real data from n8n) // ------------------------------------------ import React, { useEffect, useState } from 'react' import StatusBadge from '../components/StatusBadge' import { API } from '../lib/api' import { ArrowRight, BookOpen, Gift, Printer, RefreshCw, Wallet, Download, PlusCircle, Upload } from 'lucide-react' function Card({ children }){ return
{children}
} function Panel({ title, actionLabel, actionHref, right, children }){ return (

{title}

{right || null} {actionLabel && actionHref && ( {actionLabel} )}
{children}
) } function QuickAction({ icon, title, desc, href='#' }){ return (
{icon}
{title}
{desc}
) } export default function DashboardPage(){ const [loading, setLoading] = useState(true) const [summary, setSummary] = useState({ user:{name:'—', tokenBalance:0}, stories:[], orders:[], donations:[] }) async function load(){ setLoading(true) try{ const data = await API.dashboard() setSummary(data) } finally { setLoading(false) } } useEffect(()=>{ load() },[]) const { user, stories, orders, donations } = summary return (

{user?.name || 'Halo, Pengguna'}

Buat buku cerita personal, unduh PDF, atau cetak fisik dengan program Buy 1 Donate 1.

Buat Cerita Baru Top Up Token
Saldo Token
{user?.tokenBalance ?? 0}
1 Token = 1x Generate PDF
Cerita Selesai
{stories?.length ?? 0}
Total semua waktu
Order Cetak Aktif
{orders?.filter(o=>o.status!=='Completed').length ?? 0}
Sedang diproses
Donasi Tersalurkan
{donations?.length ?? 0}
Buy 1 Donate 1
} title="Buat Cerita" desc="Masukkan nama anak & pilih tema" href="/create"/> } title="Upload Foto" desc="Opsional untuk personalisasi" href="/upload"/> } title="Unduh PDF" desc="Gunakan token kamu" href="/downloads"/> } title="Cetak Fisik" desc="Buy 1 Donate 1" href="/print"/>
    {(stories||[]).slice(0,5).map((s)=>(
  • {s.title}
    {s.lang} • {s.theme} • {s.createdAt}
    Buka
  • ))}
    {(orders||[]).slice(0,5).map((o)=>(
  • {o.type}
    {o.serial} • {o.createdAt}
  • ))}
    {(donations||[]).slice(0,5).map((d)=>(
  • {d.recipient}
    {d.serial} • Update {d.updatedAt}
  • ))}
  • Gunakan token untuk mengunduh PDF. Top up kapan saja lewat menu Top Up Token.
  • Untuk cetak fisik, pastikan alamat lengkap dan nomor telepon aktif.
  • Tracking donasi tersedia melalui QR di halaman buku donasi.
Muat Ulang Data}>
Status layanan n8n & storage tampak normal. Jika ada kendala, klik tombol untuk menyegarkan data.
) } // ------------------------------------------ // File: src/pages/CreateStoryPage.jsx // ------------------------------------------ import React, { useState } from 'react' import { API } from '../lib/api' export default function CreateStoryPage(){ const [form, setForm] = useState({ childName:'', theme:'Adventure', lang:'ID/EN', illustration:'Auto' }) const [result, setResult] = useState(null) const [loading, setLoading] = useState(false) async function submit(e){ e.preventDefault(); setLoading(true); setResult(null) try{ const story = await API.genStory(form) setResult(story) } finally { setLoading(false) } } async function toPDF(){ setLoading(true) try{ const pdf = await API.genPDF({ storyId: result.storyId }) setResult(prev=>({...prev, pdf})) } finally { setLoading(false) } } return (

Buat Cerita

setForm({...form, childName:e.target.value})} className="mt-1 w-full rounded-xl border bg-white px-3 py-2" required/>
{result && (
{result.title}
{JSON.stringify(result.content_json, null, 2)}
{result.pdf?.pdfUrl && Buka PDF}
)}
) } // ------------------------------------------ // File: src/pages/UploadPage.jsx // ------------------------------------------ import React, { useState } from 'react' export default function UploadPage(){ const [file, setFile] = useState(null) function submit(e){ e.preventDefault(); alert('Upload via Signed URL n8n (implement nanti)') } return (

Upload Foto (Opsional)

setFile(e.target.files?.[0])} className="w-full rounded-xl border bg-white px-3 py-2"/>
{file &&

File dipilih: {file.name}

}
) } // ------------------------------------------ // File: src/pages/DownloadsPage.jsx // ------------------------------------------ import React, { useEffect, useState } from 'react' import { API } from '../lib/api' export default function DownloadsPage(){ const [stories, setStories] = useState([]) useEffect(()=>{ API.listStories().then(r=>setStories(r)) },[]) return (

Unduh PDF

{stories.map(s=> ( ))}
JudulSerialStatusAksi
{s.title} {s.serial || '—'} {s.status} {s.pdfUrl ? ( Download ) : ( )}
) } // ------------------------------------------ // File: src/pages/PrintCheckoutPage.jsx // ------------------------------------------ import React, { useState } from 'react' import { API } from '../lib/api' export default function PrintCheckoutPage(){ const [form, setForm] = useState({ storyId:'', name:'', phone:'', address:'', donateRecipient:'' }) const [result, setResult] = useState(null) async function submit(e){ e.preventDefault(); const r = await API.createPrintOrder(form) setResult(r) } return (

Cetak Fisik — Buy 1 Donate 1

setForm({...form, storyId:e.target.value})} className="mt-1 w-full rounded-xl border bg-white px-3 py-2" required/>
setForm({...form, name:e.target.value})} className="mt-1 w-full rounded-xl border bg-white px-3 py-2" required/>
setForm({...form, phone:e.target.value})} className="mt-1 w-full rounded-xl border bg-white px-3 py-2" required/>
setForm({...form, donateRecipient:e.target.value})} className="mt-1 w-full rounded-xl border bg-white px-3 py-2"/>
{result && (
Order ID: {result.orderId}
Serial Pembeli: {result.serial_1}
Serial Donasi: {result.serial_2}
Status: {result.status}
)}
) } // ------------------------------------------ // File: src/pages/OrdersPage.jsx // ------------------------------------------ import React, { useEffect, useState } from 'react' import { API } from '../lib/api' import StatusBadge from '../components/StatusBadge' export default function OrdersPage(){ const [orders, setOrders] = useState([]) useEffect(()=>{ API.listOrders().then(setOrders) },[]) return (

Riwayat Order

{orders.map(o=> ( ))}
SerialJenisStatusTanggal
{o.serial} {o.type} {o.createdAt}
) } // ------------------------------------------ // File: src/pages/StoriesPage.jsx // ------------------------------------------ import React, { useEffect, useState } from 'react' import { API } from '../lib/api' import StatusBadge from '../components/StatusBadge' export default function StoriesPage(){ const [stories, setStories] = useState([]) useEffect(()=>{ API.listStories().then(setStories) },[]) return (

Cerita Saya

{stories.map(s=> ( ))}
JudulTemaBahasaStatusCreated
{s.title} {s.theme} {s.lang} {s.createdAt}
) } // ------------------------------------------ // File: src/pages/DonationsPage.jsx // ------------------------------------------ import React, { useEffect, useState } from 'react' import { API } from '../lib/api' import StatusBadge from '../components/StatusBadge' export default function DonationsPage(){ const [donations, setDonations] = useState([]) useEffect(()=>{ API.listDonations().then(setDonations) },[]) return (

Tracking Donasi

{donations.map(d=> ( ))}
SerialPenerimaStatusUpdateQR
{d.serial} {d.recipient} {d.updatedAt} {d.qrUrl ? Buka : '—'}
) } // ------------------------------------------ // File: src/pages/TopUpPage.jsx // ------------------------------------------ import React, { useState } from 'react' import { API } from '../lib/api' export default function TopUpPage(){ const [amount, setAmount] = useState(1) const [result, setResult] = useState(null) async function submit(e){ e.preventDefault(); const r = await API.topupToken({ tokens: amount }); setResult(r) } return (

Top Up Token

setAmount(parseInt(e.target.value||'1'))} className="mt-1 w-full rounded-xl border bg-white px-3 py-2"/>
{result && (
Invoice: {result.invoiceId}
Metode: {result.method}
Total: {result.total}
{result.qrisUrl && Bayar via QRIS}
)}
) } // ------------------------------------------ // File: src/pages/LoginPage.jsx // ------------------------------------------ import React, { useState } from 'react' import { useNavigate } from 'react-router-dom' import { useAuth } from '../lib/auth' export default function LoginPage(){ const nav = useNavigate(); const { login } = useAuth() const [email, setEmail] = useState('demo@bookoomoo.app') const [pass, setPass] = useState('demo') function submit(e){ e.preventDefault(); login({ id:1, name:'Demo User', email, tokenBalance: 3 }); nav('/') } return (
Masuk ke Bookoomoo
setEmail(e.target.value)} />
setPass(e.target.value)} />
) } // ------------------------------------------ // File: src/index.css (Tailwind base) // ------------------------------------------ @tailwind base; @tailwind components; @tailwind utilities; /* Optional: smooth fonts */ html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }