// =============================
// 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
)
}
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 (
)
}
function QuickAction({ icon, title, desc, href='#' }){
return (
)
}
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.
Saldo Token
{user?.tokenBalance ?? 0}
1 Token = 1x Generate PDF
Cerita Selesai
{stories?.length ?? 0}
Total semua waktu
{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
{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)
{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
| Judul | Serial | Status | Aksi |
{stories.map(s=> (
| {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
{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
| Serial | Jenis | Status | Tanggal |
{orders.map(o=> (
| {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
| Judul | Tema | Bahasa | Status | Created |
{stories.map(s=> (
| {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
| Serial | Penerima | Status | Update | QR |
{donations.map(d=> (
| {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
{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 (
)
}
// ------------------------------------------
// 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; }