diff --git a/bookoomoo-app/.env.example b/bookoomoo-app/.env.example new file mode 100644 index 0000000..e5bc463 --- /dev/null +++ b/bookoomoo-app/.env.example @@ -0,0 +1,2 @@ +VITE_N8N_BASE_URL=https://your-n8n.example + diff --git a/bookoomoo-app/index.html b/bookoomoo-app/index.html new file mode 100644 index 0000000..6ed21d2 --- /dev/null +++ b/bookoomoo-app/index.html @@ -0,0 +1,13 @@ + + + + + + Bookoomoo + + +
+ + + + diff --git a/bookoomoo-app/package.json b/bookoomoo-app/package.json new file mode 100644 index 0000000..a17d8cd --- /dev/null +++ b/bookoomoo-app/package.json @@ -0,0 +1,25 @@ +{ + "name": "bookoomoo-app", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "lucide-react": "^0.469.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.41", + "tailwindcss": "^3.4.14", + "vite": "^5.4.8" + } +} + diff --git a/bookoomoo-app/postcss.config.js b/bookoomoo-app/postcss.config.js new file mode 100644 index 0000000..b4a6220 --- /dev/null +++ b/bookoomoo-app/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + diff --git a/bookoomoo-app/src/App.jsx b/bookoomoo-app/src/App.jsx new file mode 100644 index 0000000..fbf00fc --- /dev/null +++ b/bookoomoo-app/src/App.jsx @@ -0,0 +1,48 @@ +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 ( + + } /> + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + ) +} + diff --git a/bookoomoo-app/src/components/Layout.jsx b/bookoomoo-app/src/components/Layout.jsx new file mode 100644 index 0000000..071c4b3 --- /dev/null +++ b/bookoomoo-app/src/components/Layout.jsx @@ -0,0 +1,53 @@ +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 +} + diff --git a/bookoomoo-app/src/components/StatusBadge.jsx b/bookoomoo-app/src/components/StatusBadge.jsx new file mode 100644 index 0000000..2ff8d3b --- /dev/null +++ b/bookoomoo-app/src/components/StatusBadge.jsx @@ -0,0 +1,14 @@ +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} +} + diff --git a/bookoomoo-app/src/index.css b/bookoomoo-app/src/index.css new file mode 100644 index 0000000..3657a67 --- /dev/null +++ b/bookoomoo-app/src/index.css @@ -0,0 +1,33 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Theme tokens */ +:root { + --bg: 255 255 255; + --surface: 255 255 255; + --text: 17 24 39; + --primary: 255 115 0; /* orange */ + --accent: 14 165 233; /* sky */ + --muted: 100 116 139; +} +.dark { + --bg: 15 23 42; + --surface: 2 6 23; + --text: 241 245 249; + --primary: 255 140 66; + --accent: 56 189 248; + --muted: 148 163 184; +} + +@layer base { + html { font-family: Inter, system-ui, ui-sans-serif, sans-serif; } + body { @apply bg-[rgb(var(--bg))] text-[rgb(var(--text))] antialiased; } +} + +@layer components { + .btn { @apply inline-flex items-center justify-center rounded-xl px-4 py-2 font-medium transition; } + .btn-primary { @apply btn bg-[rgb(var(--primary))] text-white hover:brightness-110 active:brightness-90; } + .card { @apply rounded-2xl border bg-white p-4 shadow-sm; } +} + diff --git a/bookoomoo-app/src/lib/api.js b/bookoomoo-app/src/lib/api.js new file mode 100644 index 0000000..dc87179 --- /dev/null +++ b/bookoomoo-app/src/lib/api.js @@ -0,0 +1,25 @@ +export const API = { + BASE_URL: import.meta.env.VITE_N8N_BASE_URL || '', + + 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() + }, + + 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) }, + 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) }, +} + diff --git a/bookoomoo-app/src/lib/auth.jsx b/bookoomoo-app/src/lib/auth.jsx new file mode 100644 index 0000000..0b7e910 --- /dev/null +++ b/bookoomoo-app/src/lib/auth.jsx @@ -0,0 +1,16 @@ +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) } + diff --git a/bookoomoo-app/src/main.jsx b/bookoomoo-app/src/main.jsx new file mode 100644 index 0000000..4a30123 --- /dev/null +++ b/bookoomoo-app/src/main.jsx @@ -0,0 +1,17 @@ +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( + + + + + + + +) + diff --git a/bookoomoo-app/src/pages/CreateStoryPage.jsx b/bookoomoo-app/src/pages/CreateStoryPage.jsx new file mode 100644 index 0000000..fbdbe04 --- /dev/null +++ b/bookoomoo-app/src/pages/CreateStoryPage.jsx @@ -0,0 +1,10 @@ +import React from 'react' +export default function CreateStoryPage(){ + return ( +
+

Buat Cerita

+

Form pembuatan cerita akan ditempatkan di sini.

+
+ ) +} + diff --git a/bookoomoo-app/src/pages/DashboardPage.jsx b/bookoomoo-app/src/pages/DashboardPage.jsx new file mode 100644 index 0000000..cdd7e0d --- /dev/null +++ b/bookoomoo-app/src/pages/DashboardPage.jsx @@ -0,0 +1,154 @@ +import React, { useEffect, useState } from 'react' +import StatusBadge from '../components/StatusBadge' +import { API } from '../lib/api' +import { ArrowRight, BookOpen, Gift, Printer, 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) + } catch (e) { + // fallback demo data + setSummary({ + user: { name: 'Pengguna Demo', tokenBalance: 3 }, + stories: [ + { id: 's1', title: 'Petualangan Kucing', lang: 'ID', theme: 'Hewan', createdAt: '2025-08-10', status: 'PDF Ready' }, + ], + orders: [ + { id: 'o1', type: 'Softcover A5', serial: 'INV-001', createdAt: '2025-08-09', status: 'In Production' }, + ], + donations: [ + { id: 'd1', beneficiary: 'PA Tunas Bangsa', createdAt: '2025-08-08' }, + ], + }) + } 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.beneficiary || 'Penerima'}
    +
    {d.createdAt}
    +
    +
  • ))} +
+
+
+
+
+ ) +} + diff --git a/bookoomoo-app/src/pages/DonationsPage.jsx b/bookoomoo-app/src/pages/DonationsPage.jsx new file mode 100644 index 0000000..30a4849 --- /dev/null +++ b/bookoomoo-app/src/pages/DonationsPage.jsx @@ -0,0 +1,10 @@ +import React from 'react' +export default function DonationsPage(){ + return ( +
+

Donasi

+

Tracking program Buy 1 Donate 1.

+
+ ) +} + diff --git a/bookoomoo-app/src/pages/DownloadsPage.jsx b/bookoomoo-app/src/pages/DownloadsPage.jsx new file mode 100644 index 0000000..9815898 --- /dev/null +++ b/bookoomoo-app/src/pages/DownloadsPage.jsx @@ -0,0 +1,10 @@ +import React from 'react' +export default function DownloadsPage(){ + return ( +
+

Unduh PDF

+

Daftar PDF tersedia untuk diunduh.

+
+ ) +} + diff --git a/bookoomoo-app/src/pages/LoginPage.jsx b/bookoomoo-app/src/pages/LoginPage.jsx new file mode 100644 index 0000000..f3716a5 --- /dev/null +++ b/bookoomoo-app/src/pages/LoginPage.jsx @@ -0,0 +1,28 @@ +import React from 'react' +import { useNavigate } from 'react-router-dom' +import { useAuth } from '../lib/auth' + +export default function LoginPage(){ + const { login } = useAuth() + const nav = useNavigate() + function onSubmit(e){ + e.preventDefault() + const form = new FormData(e.currentTarget) + const name = form.get('name') || 'Pengguna' + login({ name, tokenBalance: 0 }) + nav('/') + } + return ( +
+
+

Masuk

+ + +
+
+ ) +} + diff --git a/bookoomoo-app/src/pages/OrdersPage.jsx b/bookoomoo-app/src/pages/OrdersPage.jsx new file mode 100644 index 0000000..f5946c2 --- /dev/null +++ b/bookoomoo-app/src/pages/OrdersPage.jsx @@ -0,0 +1,10 @@ +import React from 'react' +export default function OrdersPage(){ + return ( +
+

Order

+

Riwayat order dan status produksi.

+
+ ) +} + diff --git a/bookoomoo-app/src/pages/PrintCheckoutPage.jsx b/bookoomoo-app/src/pages/PrintCheckoutPage.jsx new file mode 100644 index 0000000..695b691 --- /dev/null +++ b/bookoomoo-app/src/pages/PrintCheckoutPage.jsx @@ -0,0 +1,10 @@ +import React from 'react' +export default function PrintCheckoutPage(){ + return ( +
+

Cetak Fisik

+

Pilih opsi cetak dan checkout.

+
+ ) +} + diff --git a/bookoomoo-app/src/pages/StoriesPage.jsx b/bookoomoo-app/src/pages/StoriesPage.jsx new file mode 100644 index 0000000..da40a77 --- /dev/null +++ b/bookoomoo-app/src/pages/StoriesPage.jsx @@ -0,0 +1,10 @@ +import React from 'react' +export default function StoriesPage(){ + return ( +
+

Cerita

+

Koleksi cerita kamu.

+
+ ) +} + diff --git a/bookoomoo-app/src/pages/TopUpPage.jsx b/bookoomoo-app/src/pages/TopUpPage.jsx new file mode 100644 index 0000000..bc270e8 --- /dev/null +++ b/bookoomoo-app/src/pages/TopUpPage.jsx @@ -0,0 +1,10 @@ +import React from 'react' +export default function TopUpPage(){ + return ( +
+

Top Up

+

Isi saldo token untuk generate PDF.

+
+ ) +} + diff --git a/bookoomoo-app/src/pages/UploadPage.jsx b/bookoomoo-app/src/pages/UploadPage.jsx new file mode 100644 index 0000000..494e765 --- /dev/null +++ b/bookoomoo-app/src/pages/UploadPage.jsx @@ -0,0 +1,10 @@ +import React from 'react' +export default function UploadPage(){ + return ( +
+

Upload Foto

+

Unggah foto untuk personalisasi cerita.

+
+ ) +} + diff --git a/bookoomoo-app/tailwind.config.js b/bookoomoo-app/tailwind.config.js new file mode 100644 index 0000000..e40a94b --- /dev/null +++ b/bookoomoo-app/tailwind.config.js @@ -0,0 +1,26 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './index.html', + './src/**/*.{js,jsx,ts,tsx}', + ], + theme: { + extend: { + colors: { + bg: 'rgb(var(--bg))', + surface: 'rgb(var(--surface))', + text: 'rgb(var(--text))', + primary: 'rgb(var(--primary))', + accent: 'rgb(var(--accent))', + muted: 'rgb(var(--muted))', + }, + fontFamily: { + sans: ['Inter', 'system-ui', 'ui-sans-serif', 'sans-serif'], + display: ['Poppins', 'Inter', 'ui-sans-serif', 'sans-serif'], + }, + }, + }, + darkMode: 'class', + plugins: [], +} + diff --git a/bookoomoo-app/vite.config.js b/bookoomoo-app/vite.config.js new file mode 100644 index 0000000..54164ce --- /dev/null +++ b/bookoomoo-app/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], +}) +