/* ════════════════════════════════════════════════════════════════ Cây Bồ Đề — Mobile App (bright saffron + white, Bchannel style) No emoji — clean SVG line icons. "APP LÀM THỬ FULL": mọi chức năng thao tác được, state thật qua module store (pub/sub + localStorage 'sdtd_app_v1'). Persona: Trần Minh An (pháp danh Minh An). ════════════════════════════════════════════════════════════════ */ const { useState, useEffect, useRef } = React; const C = { bg: '#fffdf5', // near-white content cream: '#fcf2d2', card: '#ffffff', card2: '#fff8e8', line: '#f0e2bd', saffron:'#f59000', saffronD:'#e07700', gold: '#fbc117', amber: '#ffbe3d', brown: '#97561a', ink: '#3d2509', soft: '#7a521f', faint: '#a9824a', white: '#ffffff', }; const F = "'Manrope', system-ui, sans-serif"; // ── SVG icon set (stroke, currentColor) ── function Icon({ name, size = 22, fill = false, color = 'currentColor', sw = 1.9 }) { const p = { home: , award: , plus: , temple: , person: , leaf: , bell: , check: , shield: , qr: , chevron: , book: , hands: , connect: , clock: , pin: , sparkle: , calendar: , gift: , meditate: , drop: , star: , send: , chat: , camera: , lock: , edit: , refresh: , }[name]; return ( {p} ); } const Leaf = ({ s = 15, color = C.saffron }) => ( ); function Card({ children, style, onClick }) { return
{children}
; } // header used inside white content function Header({ title, sub, back }) { return (
{back && }

{sub}

{title}

); } function Avatar({ size = 38, icon = 'meditate' }) { return (
); } function Ring({ pct, size = 92, label, value }) { const r = size / 2 - 7, c = 2 * Math.PI * r; return (
{value} {label}
); } // rounded saffron top banner behind status bar / title function TopBanner({ children, h = 150 }) { return (
{children}
); } // ════════ BODHI TREE GROWTH ════════ // Visualizes accumulated Lá Bồ Đề as a growing bodhi tree. // Ngưỡng cấp (cấp 1..6): 0 / 200 / 600 / 1200 / 2000 / 3000 Lá. const TREE_STAGES = [ { min: 0, key: 'Hạt giống', leaves: 0, trunk: 0, desc: 'Hành trình bắt đầu từ một hạt giống thiện tâm. Hãy gieo thiện hành đầu tiên.' }, { min: 200, key: 'Nảy mầm', leaves: 3, trunk: 28, desc: 'Mầm xanh đã nhú. Mỗi thiện hành là một giọt nước lành nuôi cây lớn lên.' }, { min: 600, key: 'Cây non', leaves: 9, trunk: 54, desc: 'Cây non vươn mình. Bạn đang đi đúng hướng trên con đường tỉnh thức.' }, { min: 1200, key: 'Cây vững', leaves: 18, trunk: 78, desc: 'Thân cây đã vững. Tán lá ngày một sum suê theo công đức của bạn.' }, // trunk cấp 5–6 giữ ≤ 86 để tán còn headroom trong viewport vuông (xem clamp // spread trong BodhiTree) — trunk 96/110 cũ đẩy tâm tán lên sát mép, lá bị cắt. { min: 2000, key: 'Tỏa bóng', leaves: 30, trunk: 80, desc: 'Cây bắt đầu tỏa bóng mát, che chở và lan tỏa thiện lành đến mọi người.' }, { min: 3000, key: 'Cây Bồ Đề', leaves: 46, trunk: 86, desc: 'Cây Bồ Đề sum suê — biểu tượng cho hành trình bền bỉ vun bồi từ bi và trí tuệ.' }, ]; function treeStage(leaf) { let s = 0; for (let i = 0; i < TREE_STAGES.length; i++) if (leaf >= TREE_STAGES[i].min) s = i; return s; } function BodhiTree({ leaf = 0, size = 150 }) { const si = treeStage(leaf); const st = TREE_STAGES[si]; const cx = size / 2, groundY = size * 0.86; // deterministic leaf positions around a canopy center const canopyY = groundY - st.trunk * (size / 150) - size * 0.06; // clamp độ xòe tán theo headroom còn lại phía trên tâm tán — thân càng cao // (cấp 5–6) tán càng phải gọn lại, để lá (kể cả bán kính hình lá) không văng // khỏi viewport (ly < 0). const spread = Math.min(1, (canopyY - size * 0.06) / (size * 0.30)); const leaves = []; const n = st.leaves; for (let i = 0; i < n; i++) { const ang = (i * 137.5) * Math.PI / 180; // golden-angle phyllotaxis const rad = ((size * 0.05) + Math.sqrt(i / Math.max(n, 1)) * size * 0.28) * spread; const lx = cx + Math.cos(ang) * rad; const ly = canopyY + Math.sin(ang) * rad * 0.82; const rot = (ang * 180 / Math.PI) + 90; leaves.push({ lx, ly, rot, s: 0.42 + (i % 3) * 0.06 }); } const sc = size / 150; return ( {/* ground */} {si === 0 ? ( // seed ) : ( {/* trunk */} {/* a couple of branches for bigger stages */} {si >= 3 && } {si >= 3 && } {/* leaves — each a bodhi-leaf heart-drop shape */} {leaves.map((lf, i) => ( ))} )} ); } /* ════════════════════════════════════════════════════════════════ STATE CORE — module store (pub/sub) + persist localStorage Key: 'sdtd_app_v1'. SSR/private-mode an toàn (try/catch → memory). Số chuẩn toàn dự án (KHÔNG phá): 1.248 Lá · 310/500 (62%) · #214 (GHIM — demo) · Top-3 Gala: Minh Tuệ 4.820 / Diệu Tâm 4.510 / Nguyên Hạnh 4.205 · Top 8%. ════════════════════════════════════════════════════════════════ */ const STORE_KEY = 'sdtd_app_v1'; const MONTH_CAP = 500; // trần Lá/tháng — cơ chế Phòng Ác // Giai đoạn Khởi Động (T3–T9/2026) — đếm ngược tính live từ ngày hiện tại, không hardcode const PHASE_START = new Date(2026, 2, 1); // 01/3/2026 const PHASE_END = new Date(2026, 8, 30); // đóng sổ 30/9/2026 // Mốc dự án Cầu Nối: mỗi mốc mang Lá riêng (15–50) — đồng bộ roadmap Webapp.html const uid = () => 'x' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); const fmt = n => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, '.'); const nowTime = () => { const d = new Date(); return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); }; const todayKey = () => { const d = new Date(); return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate(); }; // 11 loại thiện hành (khớp 11 lĩnh vực hệ thống) — bảng điểm 15–80 Lá/loại. // Số Lá ĐỒNG BỘ với các record đã chốt toàn dự án: // khóa thiền/khóa tu 50 (seed h1, notification n1, Webapp 'Khóa thiền 3 ngày · +50') // giảng pháp 40 (seed h3, Webapp) · hiến máu 30 (seed h4, TEMPLE_INFO Chùa Hương Sắc, Webapp) // xuất bản ISBN 80 (Webapp 'Xuất bản tập thơ ISBN · +80') — các loại còn lại scale ×3 tương ứng. const DEED_TYPES = [ ['hands', 'Thiện nguyện cộng đồng', '≥ 10 người hưởng lợi', 60], ['meditate', 'Khóa thiền / khóa tu', '≥ 3 ngày tập trung', 50], ['book', 'Xuất bản tác phẩm', 'Có mã ISBN', 80], ['person', 'Giảng pháp công cộng', 'Buổi giảng đại chúng', 40], ['drop', 'Hiến máu nhân đạo', 'Mỗi lần hiến hợp lệ', 30], ['star', 'Giáo dục — lớp học 0 đồng', 'Mỗi lớp/khóa hoàn thành', 45], ['leaf', 'Bảo vệ môi trường', 'Trồng cây / dọn rác có minh chứng', 30], ['sparkle', 'Bảo tồn di sản', 'Tham gia trùng tu / số hóa', 45], ['connect', 'Truyền thông Phật pháp số', 'Nội dung được duyệt đăng', 30], ['gift', 'Công quả tại chùa', 'Mỗi buổi công quả', 15], ['temple', 'Hộ trì sự kiện Phật giáo', 'Mỗi sự kiện phục vụ', 30], ]; // Top-3 chuẩn (đồng bộ Portal & Webapp) — không đổi thứ tự/số liệu const GALA_TOP = [['Thầy Minh Tuệ', '4.820', 1], ['Sư cô Diệu Tâm', '4.510', 2], ['Cư sĩ Nguyên Hạnh', '4.205', 3]]; const GALA_REST = [['Cư sĩ Thiện Đức', '3.980'], ['Ni sư Hương Trang', '3.760'], ['Nhóm Hương Sắc Bồ Đề', '3.540']]; const TEMPLE_INFO = { 'Chùa Quán Sứ': { place: 'Hà Nội', dist: '0.4 km', founded: '1934', address: '73 Quán Sứ, Hoàn Kiếm', members: '24.100', desc: 'Trụ sở Trung ương Giáo hội Phật giáo Việt Nam — trung tâm Phật giáo lớn của miền Bắc.', acts: [ // việc con (đợt tuyển trợ giảng tháng 7) của dự án Cầu Nối p1 'Lớp tiếng Anh // miễn phí cho trẻ em' — đặt tên riêng để 2 thực thể không bị đọc nhầm là trùng lặp { t: 'Trợ giảng lớp tiếng Anh — đợt T7', req: 'Tình nguyện viên dạy / trợ giảng (T7, CN)', due: '30/06/2026', leaf: 80, type: 'Giáo dục', ic: 'book', status: 'open' }, { t: 'Khóa tu Bát Quan Trai', req: 'Đăng ký tham dự trọn một ngày', due: '15/06/2026', leaf: 50, type: 'Tu học', ic: 'meditate', status: 'open' }, { t: 'Phát quà Trung thu cho trẻ vùng cao', req: 'Đóng góp / đồng hành chuyến đi', due: '20/09/2026', leaf: 40, type: 'Thiện nguyện', ic: 'hands', status: 'soon' }, ] }, 'Chùa Trấn Quốc': { place: 'Tây Hồ', dist: '1.2 km', founded: '541', address: 'Thanh Niên, Tây Hồ', members: '18.700', desc: 'Ngôi chùa cổ nhất Hà Nội, hơn 1.500 năm tuổi bên Hồ Tây.', acts: [ { t: 'Khóa thiền cuối tuần', req: 'Tham dự ≥ 2 buổi', due: '10/06/2026', leaf: 45, type: 'Tu học', ic: 'meditate', status: 'open' }, { t: 'Trùng tu vườn tháp cổ', req: 'Đóng góp công đức tu bổ', due: '31/07/2026', leaf: 70, type: 'Di sản', ic: 'sparkle', status: 'soon' }, ] }, 'Chùa Hương Sắc': { place: 'Hà Đông', dist: '3.8 km', founded: '1998', address: 'Phú Lương, Hà Đông', members: '9.400', desc: 'Trung tâm sinh hoạt Phật pháp và truyền thông Hương Sắc Bồ Đề.', acts: [ { t: 'Sản xuất nội dung Phật pháp số', req: 'Cộng tác viết / dựng video', due: '25/06/2026', leaf: 90, type: 'Truyền thông', ic: 'connect', status: 'open' }, { t: 'Bếp chay 0 đồng cuối tháng', req: 'Tình nguyện viên nấu / phục vụ', due: '28/06/2026', leaf: 60, type: 'Thiện nguyện', ic: 'hands', status: 'open' }, { t: 'Hiến máu nhân đạo', req: 'Mỗi lần hiến hợp lệ', due: '12/07/2026', leaf: 30, type: 'Cộng đồng', ic: 'drop', status: 'soon' }, ] }, }; // ── Thiện Tri Thức: flows + keyword matching (mượn matchFlow của thien-tri-thuc.html) // Trích dẫn dùng PLACEHOLDER (chờ Ban Văn hóa kiểm định) — TUYỆT ĐỐI không chèn bản dịch kinh thật. const CHAT_FLOWS = [ { q: 'Con đang giận anh trai…', ctx: 'sân hận · gia đình', cites: [ { src: 'Kinh Pháp Cú · Phẩm Song Yếu · câu 5', tags: ['Sân hận', 'Gia đình', 'Buông xả'] }, { src: 'Kinh Pháp Cú · Phẩm Phẫn Nộ · câu 223', tags: ['Sân hận', 'Từ bi'] } ] }, { q: 'Mất việc khiến con hoang mang', ctx: 'công việc · vô thường', cites: [ { src: 'Kinh Pháp Cú · Phẩm Vô Thường · câu 277', tags: ['Vô thường', 'Công việc'] }, { src: 'Kinh Pháp Cú · Phẩm An Lạc · câu 204', tags: ['An lạc', 'Tri túc'] } ] }, { q: 'Làm sao buông được lo âu?', ctx: 'lo âu · điều tâm', cites: [ { src: 'Kinh Pháp Cú · Phẩm Tâm · câu 35', tags: ['Lo âu', 'Điều tâm'] }, { src: 'Kinh Pháp Cú · Phẩm Hiền Trí · câu 81', tags: ['Vững chãi', 'Buông xả'] } ] }, { q: 'Con vừa mất người thân, lòng trống rỗng quá', ctx: 'mất mát · vô thường', cites: [ { src: 'Kinh Pháp Cú · Phẩm Già · câu 148', tags: ['Vô thường', 'Khổ đau'] }, { src: 'Kinh Từ Bi (Metta Sutta) · Tiểu Bộ · kệ 7–8', tags: ['Từ bi', 'Thương yêu'] } ] }, { q: 'Vợ chồng con cãi nhau mãi, mệt mỏi quá', ctx: 'gia đình · hòa hợp', cites: [ { src: 'Kinh Phước Đức (Mangala Sutta) · Tiểu Bộ · kệ 4', tags: ['Gia đình', 'Hòa hợp'] }, { src: 'Kinh Pháp Cú · Phẩm Phẫn Nộ · câu 223', tags: ['Từ bi', 'Nhẫn nhịn'] } ] }, ]; const CHAT_FALLBACK = { ctx: 'đời thường', cites: [ { src: 'Kinh Pháp Cú · Phẩm Tâm · câu 35', tags: ['Điều tâm', 'Tỉnh thức'] }, { src: 'Kinh Pháp Cú · Phẩm Vô Thường · câu 277', tags: ['Vô thường', 'Quán chiếu'] } ] }; const CHAT_KEYWORDS = [ { re: /giận|sân|tức|hận|ghét/i, flow: 0 }, { re: /việc|nghề|thất nghiệp|sa thải|công ty|sự nghiệp/i, flow: 1 }, { re: /lo|sợ|bất an|căng thẳng|áp lực|stress/i, flow: 2 }, { re: /mất|tang|qua đời|ra đi|người thân|trống rỗng/i, flow: 3 }, { re: /vợ|chồng|cãi|hôn nhân|ly hôn|gia đình/i, flow: 4 }, ]; function matchChatFlow(q) { const exact = CHAT_FLOWS.findIndex(f => f.q === q); if (exact >= 0) return exact; for (const k of CHAT_KEYWORDS) if (k.re.test(q)) return k.flow; return -1; // không khớp → bộ trích dẫn tổng quát } function seedState() { return { v: 3, // BUMP mỗi khi đổi schema (v3: seed p3 joined:true khớp Webapp; v2: milestones mang leaf riêng) name: 'Trần Minh An', dharma: 'Minh An', leaves: 1248, monthLeaves: 310, rank: 214, // GHIM — không đổi khi tương tác (demo) history: [ { id: 'h1', t: 'Khóa thiền 3 ngày — chùa Hoằng Pháp', date: 'Hôm qua', leaf: 50, ic: 'meditate' }, { id: 'h2', t: 'Phát cơm từ thiện · 40 suất', date: '12/05', leaf: 100, ic: 'hands' }, { id: 'h3', t: 'Giảng pháp công cộng tại đạo tràng', date: '08/05', leaf: 40, ic: 'book' }, { id: 'h4', t: 'Hiến máu nhân đạo', date: '02/05', leaf: 30, ic: 'drop' }, { id: 'h5', t: 'Check-in Chùa Hoằng Pháp', date: '28/05', leaf: 2, ic: 'temple' }, ], notifications: [ { id: 'n1', ic: 'check', tone: 'ok', t: 'Thiện hành được chùa xác thực', d: 'Khóa thiền 3 ngày · +50 Lá Bồ Đề', time: '09:41', day: 'Hôm nay', unread: true }, { id: 'n2', ic: 'connect', tone: 'gold', t: 'Dự án mới phù hợp với bạn', d: 'Lớp tiếng Anh miễn phí — Chùa Quán Sứ', time: '08:02', day: 'Hôm nay', unread: true }, { id: 'n3', ic: 'chat', tone: 'gold', t: 'Thiện Tri Thức', d: '2 trích dẫn mới cho câu hỏi bạn lưu.', time: '07:15', day: 'Hôm nay', unread: true }, { id: 'n4', ic: 'award', tone: 'gold', t: 'Bạn thăng hạng lên #214', d: 'Tăng 12 bậc trong bảng vinh danh tháng', time: '19:20', day: 'Hôm qua', unread: false }, { id: 'n5', ic: 'calendar', tone: 'gold', t: 'Sắp tới hạn nộp hồ sơ', d: 'Giai đoạn Khởi Động đóng sổ 30/9/2026', time: '14:05', day: 'Hôm qua', unread: false }, { id: 'n6', ic: 'gift', tone: 'gold', t: 'Nhà bảo trợ mới đồng hành', d: 'Mảnh Đồng Vàng được trao cho dự án bạn theo dõi', time: '11:30', day: 'Hôm qua', unread: false }, ], deeds: [], // {id, typeIdx, t, leaf, ic, approver, evidence, status:'pending'|'approved', granted?, time} checkins: [{ id: 'ck-seed', name: 'Chùa Hoằng Pháp', place: 'TP.HCM', date: '28/05/2026 · 08:10', day: '2026-5-28' }], connectTasks: [], // việc nhận từ check-in chùa (thay MY_CONNECT cũ) // Mốc & Lá/mốc PORT NGUYÊN VĂN từ roadmap Webapp.html (cùng persona Minh An): // p1 đã tham gia + 2 mốc đầu đã duyệt, p2 đã tham gia + 1 mốc đã duyệt, // p3 đã tham gia, mốc 1 đang mở — Webapp: mốc 1 'current', chưa mốc nào approved // — 2 demo khớp trạng thái. projects: [ { id: 'p1', t: 'Lớp tiếng Anh miễn phí cho trẻ em', org: 'Chùa Quán Sứ · Hà Nội', cat: 'Giáo dục', need: 'Cần 4 tình nguyện viên', ic: 'book', joined: true, completed: false, milestones: [ { t: 'Khảo sát & lập danh sách lớp', leaf: 20, done: true }, { t: 'Soạn giáo trình 8 buổi', leaf: 30, done: true }, { t: 'Giảng dạy 8 buổi học', leaf: 40, done: false }, { t: 'Tổng kết & báo cáo kết quả', leaf: 30, done: false }, ] }, { id: 'p2', t: 'Bếp ăn 0 đồng tại bệnh viện', org: 'Đạo Tràng Từ Tâm · Đà Nẵng', cat: 'Xã hội', need: 'Cần 15 tình nguyện viên', ic: 'hands', joined: true, completed: false, milestones: [ { t: 'Tập huấn an toàn thực phẩm', leaf: 15, done: true }, { t: 'Phục vụ 4 ca nấu ăn', leaf: 40, done: false }, { t: 'Vận động thêm nhà hảo tâm', leaf: 25, done: false }, ] }, { id: 'p3', t: 'Khóa tu mùa hè cho thanh thiếu niên', org: 'Chùa Ba Vàng · Quảng Ninh', cat: 'Phật pháp', need: 'Cần 8 điều phối viên', ic: 'meditate', joined: true, completed: false, milestones: [ { t: 'Chuẩn bị nội dung sinh hoạt', leaf: 20, done: false }, { t: 'Trợ giảng 3 ngày khóa tu', leaf: 50, done: false }, { t: 'Thu hoạch & chia sẻ', leaf: 20, done: false }, ] }, ], votes: [186, 142, 117], voted: null, chatLog: [{ id: 'c-seed', q: 'Con đang giận anh trai…', flow: 0 }], }; } function loadState() { try { const raw = localStorage.getItem(STORE_KEY); // Merge đè lên defaults mới: field thêm sau (cùng version) vẫn có giá trị seed, // tránh undefined.filter/map cho khách quay lại. Đổi schema sâu hơn → bump v. if (raw) { const s = JSON.parse(raw); if (s && s.v === 3 && s.notifications) return Object.assign({}, seedState(), s); } } catch (e) {} return seedState(); } let STATE = loadState(); const LISTENERS = new Set(); function saveState() { try { localStorage.setItem(STORE_KEY, JSON.stringify(STATE)); } catch (e) {} } function setState(patch) { STATE = Object.assign({}, STATE, patch); saveState(); LISTENERS.forEach(fn => { try { fn(STATE); } catch (e) {} }); } // hook — mọi screen subscribe store; SSR-safe (useEffect không chạy khi renderToString) function useStore() { const [s, setS] = useState(STATE); useEffect(() => { const fn = ns => setS(ns); LISTENERS.add(fn); setS(STATE); // sync nếu store đổi giữa render & effect return () => LISTENERS.delete(fn); }, []); return s; } // derived helpers const unreadCount = s => s.notifications.filter(n => n.unread).length; const approvedDeeds = s => s.deeds.filter(d => d.status === 'approved').length; const anyMilestoneDone = s => s.projects.some(p => p.milestones.some(m => m.done)); // DEED_TIMERS: CHỦ ĐÍCH để module-level, KHÔNG clear khi screen unmount — đây là // sai khác có cân nhắc so với câu chữ đặc tả mục 3 ("clear timeout khi unmount"): // • Ý ĐỒ AN TOÀN của đặc tả vẫn được thỏa: timer chỉ mutate module store (không setState // trực tiếp); component unmount đã gỡ listener trong useStore → không có // setState-after-unmount. // • Nếu clear theo nghĩa đen khi unmount: deed vừa gửi rồi chuyển tab trong 4s sẽ kẹt // 'pending' vĩnh viễn trong phiên — bug thật. Deed pending còn được re-arm khi reload // (xem rescheduler cuối phần actions). KHÔNG clear nếu không re-arm lại. const DEED_TIMERS = {}; const actions = { notify(ic, tone, t, d) { setState({ notifications: [{ id: uid(), ic, tone, t, d, time: nowTime(), day: 'Hôm nay', unread: true }, ...STATE.notifications] }); }, markRead(id) { setState({ notifications: STATE.notifications.map(n => n.id === id ? Object.assign({}, n, { unread: false }) : n) }); }, markAllRead() { setState({ notifications: STATE.notifications.map(n => n.unread ? Object.assign({}, n, { unread: false }) : n) }); }, // cộng Lá có trần tháng (Phòng Ác) — điểm demo đắt giá addLeaves(n, label, ic) { const room = Math.max(0, MONTH_CAP - STATE.monthLeaves); const granted = Math.min(n, room); const clipped = granted < n; setState({ leaves: STATE.leaves + granted, monthLeaves: STATE.monthLeaves + granted, history: granted > 0 ? [{ id: uid(), t: label, date: 'Hôm nay', leaf: granted, ic: ic || 'leaf' }, ...STATE.history] : STATE.history, }); if (clipped) actions.notify('shield', 'gold', 'Đã chạm trần 500 Lá/tháng — cơ chế Phòng Ác', granted > 0 ? `Chỉ cộng +${granted}/${n} Lá cho “${label}”. Phần vượt trần không tích lũy — giữ thiện hành thuần vì tâm.` : `“${label}” không được cộng Lá trong tháng này. Trần tháng giúp thiện hành thuần vì tâm, không chạy đua điểm.`); return { granted, clipped }; }, checkIn(name) { const day = todayKey(); if (STATE.checkins.some(c => c.name === name && c.day === day)) return { ok: false, dup: true }; const info = TEMPLE_INFO[name] || { place: '' }; setState({ checkins: [{ id: uid(), name, place: info.place, date: 'Hôm nay · ' + nowTime(), day }, ...STATE.checkins] }); const r = actions.addLeaves(2, 'Check-in ' + name, 'temple'); actions.notify('temple', 'ok', 'Check-in thành công', name + (r.granted > 0 ? ` · +${r.granted} Lá Bồ Đề` : ' · đã chạm trần tháng')); return { ok: true, granted: r.granted }; }, addConnectTask(name, a) { if (STATE.connectTasks.some(x => x.t === a.t)) return; setState({ connectTasks: [{ id: uid(), t: a.t, org: name + ' · ' + (TEMPLE_INFO[name] || {}).place, cat: a.type, leaf: a.leaf, need: a.req, due: a.due, ic: a.ic, fromTemple: true }, ...STATE.connectTasks] }); }, removeConnectTask(t) { setState({ connectTasks: STATE.connectTasks.filter(x => x.t !== t) }); }, submitDeed({ typeIdx, approver, evidence }) { const ty = DEED_TYPES[typeIdx]; const deed = { id: uid(), typeIdx, t: ty[1], leaf: ty[3], ic: ty[0], approver, evidence: !!evidence, status: 'pending', time: nowTime() }; setState({ deeds: [deed, ...STATE.deeds] }); actions.scheduleApproval(deed.id); return deed.id; }, scheduleApproval(id) { // auto-demo: 4s sau Trụ trì xác thực if (DEED_TIMERS[id]) return; DEED_TIMERS[id] = setTimeout(() => { delete DEED_TIMERS[id]; actions.approveDeed(id); }, 4000); }, approveDeed(id) { const d = STATE.deeds.find(x => x.id === id); if (!d || d.status !== 'pending') return; const r = actions.addLeaves(d.leaf, d.t + ' — đã xác thực', d.ic); setState({ deeds: STATE.deeds.map(x => x.id === id ? Object.assign({}, x, { status: 'approved', granted: r.granted }) : x) }); // granted=0 (trần tháng đã cạn): KHÔNG hiện '+0 Lá' — addLeaves đã tự bắn // notification trần Phòng Ác riêng, ở đây chỉ báo kết quả xác thực. actions.notify('check', 'ok', 'Trụ trì đã xác thực ✓', r.granted === 0 ? `${d.t} · Trụ trì đã xác thực — đã chạm trần tháng, Lá sẽ tính sang tháng sau` : `${d.t} · +${r.granted} Lá Bồ Đề` + (r.clipped ? ' (đã chạm trần tháng)' : '')); }, joinProject(id) { const p = STATE.projects.find(x => x.id === id); if (!p || p.joined) return; setState({ projects: STATE.projects.map(x => x.id === id ? Object.assign({}, x, { joined: true }) : x) }); const idx = Math.max(0, p.milestones.findIndex(m => !m.done)); actions.notify('connect', 'gold', 'Đã tham gia dự án', `${p.t} — mốc ${idx + 1} đã mở, hoàn thành để nhận +${p.milestones[idx].leaf} Lá.`); }, leaveProject(id) { const p = STATE.projects.find(x => x.id === id); if (!p || !p.joined) return; setState({ projects: STATE.projects.map(x => x.id === id ? Object.assign({}, x, { joined: false }) : x) }); actions.notify('connect', 'gold', 'Đã rời dự án', `${p.t} — tiến độ mốc được giữ lại, bạn có thể quay lại bất cứ lúc nào.`); }, completeMilestone(pid) { const p = STATE.projects.find(x => x.id === pid); if (!p || !p.joined || p.completed) return; const idx = p.milestones.findIndex(m => !m.done); if (idx < 0) return; const ms = p.milestones.map((m, i) => i === idx ? Object.assign({}, m, { done: true }) : m); const completed = ms.every(m => m.done); setState({ projects: STATE.projects.map(x => x.id === pid ? Object.assign({}, x, { milestones: ms, completed }) : x) }); const r = actions.addLeaves(p.milestones[idx].leaf, `Mốc “${p.milestones[idx].t}” — ${p.t}`, 'check'); if (completed) actions.notify('sparkle', 'ok', 'Hoàn thành dự án', `${p.t} — bạn đã đi trọn ${ms.length} mốc. Lành thay!`); else actions.notify('connect', 'ok', `Hoàn thành mốc ${idx + 1}`, `${p.t} · +${r.granted} Lá — mốc ${idx + 2} đã mở.`); }, vote(i) { if (STATE.voted === i) return; const prev = STATE.voted; setState({ votes: STATE.votes.map((n, j) => j === i ? n + 1 : j === prev ? n - 1 : n), voted: i }); actions.notify('award', 'gold', 'Đã ghi nhận phiếu bình chọn', `Bạn bình chọn cho ${GALA_TOP[i][0]} — mỗi người 1 phiếu (demo).`); }, askChat(q) { const t = (q || '').trim(); if (!t) return; setState({ chatLog: [...STATE.chatLog, { id: uid(), q: t, flow: matchChatFlow(t) }] }); }, setProfile(name, dharma) { setState({ name: (name || '').trim() || STATE.name, dharma: (dharma || '').trim() || STATE.dharma }); }, resetDemo() { Object.keys(DEED_TIMERS).forEach(k => { clearTimeout(DEED_TIMERS[k]); delete DEED_TIMERS[k]; }); try { localStorage.removeItem('sdtd_cover'); localStorage.removeItem('sdtd_avatar'); } catch (e) {} STATE = seedState(); saveState(); LISTENERS.forEach(fn => { try { fn(STATE); } catch (e) {} }); actions.notify('sparkle', 'gold', 'Chào mừng đến với demo Cây Bồ Đề', 'Dữ liệu demo đã được đặt lại về trạng thái ban đầu.'); }, }; // reload trang khi còn deed chờ duyệt (persist) → lên lịch duyệt lại try { STATE.deeds.filter(d => d.status === 'pending').forEach(d => actions.scheduleApproval(d.id)); } catch (e) {} // ════════ HOME ════════ function HomeScreen({ nav }) { const s = useStore(); const unread = unreadCount(s); const monthPct = Math.min(1, s.monthLeaves / MONTH_CAP); const capped = s.monthLeaves >= MONTH_CAP; const si = treeStage(s.leaves); const st = TREE_STAGES[si]; const next = TREE_STAGES[si + 1]; const treePct = next ? Math.min(1, (s.leaves - st.min) / (next.min - st.min)) : 1; const latestApproved = s.deeds.find(d => d.status === 'approved'); const openProjects = s.projects.filter(p => !p.completed).slice(0, 2); // đếm ngược Giai đoạn Khởi Động — tính live, không hardcode số ngày/% (số này KHÔNG thuộc bộ số GHIM) const phaseLeft = Math.max(0, Math.ceil((PHASE_END - Date.now()) / 86400000)); const phasePct = Math.max(0, Math.min(100, Math.round((Date.now() - PHASE_START) / (PHASE_END - PHASE_START) * 100))); return (

Nam mô A Di Đà Phật

{s.dharma}

nav && nav('notifications')} style={{ position: 'relative', width: 40, height: 40, borderRadius: 100, background: 'rgba(255,255,255,0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}> {unread > 0 && {unread > 9 ? '9+' : unread}}
{/* balance — đọc store, cập nhật live */}

Tổng Lá Bồ Đề

{fmt(s.leaves)}
{/* pills overlapping — #214 GHIM (demo), 2 pill kia đọc store */}
nav && nav('gala')} style={{ flex: 1, textAlign: 'center', padding: '12px 0' }}>

Xếp hạng

#{s.rank}

nav && nav('record')} style={{ flex: 1, textAlign: 'center', padding: '12px 0' }}>

Trần còn lại

{Math.max(0, MONTH_CAP - s.monthLeaves)}

nav && nav('record')} style={{ flex: 1, textAlign: 'center', padding: '12px 0' }}>

Tháng này

+{s.monthLeaves}

{/* trần Lá/tháng — cơ chế Phòng Ác (live từ store; nổi bật khi chạm trần) */}
{s.monthLeaves} / {MONTH_CAP} Lá tháng này {Math.round(monthPct * 100)}%

{capped ? 'Đã chạm trần tháng — cơ chế Phòng Ác đang giữ thiện hành thuần vì tâm' : `Trần ${MONTH_CAP} Lá/tháng — cơ chế Phòng Ác`}

{/* Thiện Tri Thức — hỏi chuyện đời, nhận lời kinh (mở ChatScreen — xem ghi chú điều hướng ở TabBar) */}
nav && nav('chat')} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 15, background: 'linear-gradient(135deg,#fff6e3,#ffe9bf)', borderColor: 'rgba(245,144,0,0.3)' }}>
🪷

Thiện Tri Thức

Hỏi chuyện đời, nhận lời kinh — bạn tự đọc, tự chiêm nghiệm

{/* cây bồ đề — đổi hình theo cấp (6 mức), nhãn cấp + tiến độ lên cấp kế */}

Cây Bồ Đề của bạn

Cấp {si + 1}/6 · {st.key}

{st.desc}

{next ? Còn {fmt(next.min - s.leaves)} Lá để lên cấp {si + 2} · {next.key} : Cây đã viên mãn — lành thay!}

{/* campaign phase */}
Giai đoạn: Khởi Động T3 – T9 / 2026

Đang nhận hồ sơ & ghi nhận tiến độ dự án. Điểm tiếp tục tích lũy.

Đóng sổ 30/9/2026 · {phaseLeft > 0 ? còn {phaseLeft} ngày : đã đóng sổ}

{/* quick actions */}
nav && nav('checkin')} style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 11, padding: 14 }}>

Check-in

Tích lũy Lá

nav && nav('connect')} style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 11, padding: 14 }}>

Cầu Nối

Gieo duyên lành

{/* đang cần hỗ trợ — đọc projects từ store */}

Đang cần hỗ trợ

nav && nav('connect')} style={{ color: C.saffronD, fontSize: 13, fontWeight: 700, cursor: 'pointer' }}>Xem tất cả ›
{openProjects.map(p => ( nav && nav('connect')} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 14 }}>

{p.t}

{p.joined ? `Đang tham gia · ${p.milestones.filter(m => m.done).length}/${p.milestones.length} mốc` : p.org}

~{p.milestones.filter(m => !m.done).reduce((a, m) => a + m.leaf, 0)}
))}
{/* verify chip — hiện xác thực mới nhất từ store (fallback seed) */}
nav && nav('record')} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 14, borderColor: 'rgba(245,144,0,0.3)' }}>

Đã được chùa xác thực

{latestApproved ? `${latestApproved.t} · hôm nay` : 'Chùa Hoằng Pháp · 09:41 hôm nay'}

{/* recent — lịch sử Lá từ store */}

Thiện hành gần đây

{s.history.slice(0, 4).map(a => (

{a.t}

{a.date}

+{a.leaf}
))}
); } // ════════ CHECK-IN ════════ const NEARBY_TEMPLES = [['Chùa Quán Sứ', '0.4 km', 'Hà Nội'], ['Chùa Trấn Quốc', '1.2 km', 'Tây Hồ'], ['Chùa Hương Sắc', '3.8 km', 'Hà Đông']]; function CheckInScreen({ nav }) { const s = useStore(); const [active, setActive] = useState(null); // temple name → view chi tiết const [phase, setPhase] = useState('idle'); // idle | qr | gps const [gpsDone, setGpsDone] = useState(false); const [success, setSuccess] = useState(null); // { name, granted } const [toast, setToast] = useState(null); const timers = useRef([]); useEffect(() => () => { timers.current.forEach(clearTimeout); }, []); // clear mọi timer khi unmount const later = (fn, ms) => { timers.current.push(setTimeout(fn, ms)); }; const showToast = msg => { setToast(msg); later(() => setToast(null), 2800); }; function tryCheckIn(name) { const r = actions.checkIn(name); if (!r.ok) { showToast('Hôm nay bạn đã check-in chùa này — hẹn ngày mai nhé.'); return false; } setSuccess({ name, granted: r.granted }); return true; } function startQR() { if (phase !== 'idle') return; setSuccess(null); setPhase('qr'); later(() => { setPhase('idle'); const day = todayKey(); const pick = NEARBY_TEMPLES.find(t => !STATE.checkins.some(c => c.name === t[0] && c.day === day)); if (!pick) { showToast('Hôm nay bạn đã check-in cả 3 chùa lân cận.'); return; } tryCheckIn(pick[0]); }, 1500); // demo quét 1.5s } function startGPS() { if (phase !== 'idle') return; setSuccess(null); setPhase('gps'); later(() => { setPhase('idle'); setGpsDone(true); }, 1300); // demo định vị 1.3s } // ── temple detail (sau check-in / từ lịch sử) ── if (active) { const info = TEMPLE_INFO[active]; return (
Đã check-in

{active}

{info.address} · {info.place}

Check-in để tìm hiểu chùa và nhận việc Cầu Nối

{/* temple info */}

{info.desc}

{info.founded}

Năm dựng

{info.members}

Phật tử

{info.acts.length}

Dự án / sự kiện

{/* activities — "Thêm Cầu Nối" ghi vào store.connectTasks */}

Dự án & sự kiện đang & sắp mở

Thêm vào danh sách Cầu Nối để nhận việc và đóng góp

{info.acts.map(a => { const isJoined = s.connectTasks.some(x => x.t === a.t); return (

{a.t}

{a.type} {a.status === 'open' ? '● Đang mở' : '○ Sắp mở'}

Yêu cầu: {a.req}

Hạn: {a.due}

+{a.leaf}
); })}
); } // ── default: scanner + GPS + nearby + lịch sử ── return (
nav && nav('home')} />

Check-in tại chùa để tìm hiểu thông tin chùa, nhận +2 Lá Bồ Đề và xem các dự án chùa đang & sắp mở — thêm vào Cầu Nối để nhận việc.

{/* toast nhẹ (chặn check-in trùng trong ngày) */} {toast && (

{toast}

)} {/* thành công */} {success && (

Check-in thành công · {success.name}

{success.granted > 0 ? Bạn nhận +{success.granted} Lá Bồ Đề : 'Đã chạm trần tháng — Lá không cộng thêm (Phòng Ác)'}

)} {/* QR scanner — bấm "Quét QR" để chạy demo 1.5s */}
{[[0,0],[0,1],[1,0],[1,1]].map((p,i) => ( ))}
{phase === 'qr' &&
}
{phase === 'qr' ? 'Đang quét mã QR của chùa…' : 'Quét mã QR đặt tại cổng chùa để check-in'}
{/* nearby — chọn 1 chùa để check-in (chặn trùng trong ngày) */}

Chùa lân cận {gpsDone && Đã định vị GPS}

{NEARBY_TEMPLES.map(t => { const doneToday = s.checkins.some(c => c.name === t[0] && c.day === todayKey()); return ( doneToday ? setActive(t[0]) : tryCheckIn(t[0])} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 14, cursor: 'pointer' }}>

{t[0]}

{t[2]} · {t[1]}

{doneToday ? Hôm nay ✓ : Check-in}
); })}
{/* lịch sử check-in — từ store */}

Lịch sử check-in

{s.checkins.map(c => ( TEMPLE_INFO[c.name] ? setActive(c.name) : null} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 13 }}>

{c.name}

{c.place} · {c.date}

Đã đến
))}
); } // ════════ RECORD ════════ // 11 loại thiện hành — gửi → "Đang chờ xác thực" → auto-demo 4s sau // Trụ trì xác thực ✓ + cộng Lá (qua trần tháng nếu chạm Phòng Ác). function RecordScreen() { const s = useStore(); const [sel, setSel] = useState(0); const [approver, setApprover] = useState('temple'); const [evidence, setEvidence] = useState(false); // demo: đính kèm minh chứng const [sentId, setSentId] = useState(null); // id deed vừa gửi → view xác nhận live const remaining = Math.max(0, MONTH_CAP - s.monthLeaves); const approvers = [ { key: 'temple', ic: 'temple', t: 'Trụ trì chùa xác nhận', d: 'Phù hợp hoạt động tại chùa bạn sinh hoạt' }, { key: 'council', ic: 'shield', t: 'Hội đồng Ban Văn hóa TƯ xét duyệt', d: 'Cho tác phẩm, dự án quy mô lớn' }, ]; const deedList = s.deeds.length > 0 && (

Ghi nhận của bạn

Demo: chùa xác thực tự động sau vài giây

{s.deeds.map(d => (

{d.t}

{d.time} hôm nay{d.evidence ? ' · có minh chứng' : ''}

{d.status === 'pending' ? Chờ xác thực : {d.granted > 0 ? `Đã duyệt +${d.granted}` : 'Đã duyệt · trần tháng'}}
))}
); if (sentId) { const deed = s.deeds.find(d => d.id === sentId); const approved = deed && deed.status === 'approved'; const ap = approvers.find(a => a.key === approver); return (

{approved ? 'Trụ trì đã xác thực ✓' : 'Đã gửi ghi nhận'}

{approved ? (deed.granted > 0 ? Thiện hành {deed.t} đã được duyệt — bạn nhận +{deed.granted} Lá Bồ Đề. : Thiện hành {deed.t} đã được duyệt — đã chạm trần tháng, Lá sẽ tính sang tháng sau.) : Thiện hành {deed ? deed.t : ''} của bạn đang chờ {ap.t.replace(' xác nhận', '').replace(' xét duyệt', '')} {approver === 'temple' ? 'xác nhận' : 'xét duyệt'}.}

Trạng thái: {approved ? (deed.granted > 0 ? 'Đã xác thực — Lá đã cộng' : 'Đã xác thực — đã chạm trần tháng') : 'Chờ chùa xác thực minh chứng'}

{deed && deed.evidence ? 'Đã đính kèm demo_minhchung.jpg · ' : ''}{approved ? (deed.granted > 0 ? 'Xem Trang chủ & Hồ sơ để thấy Lá cập nhật' : 'Trần 500 Lá/tháng (Phòng Ác) — Lá sẽ tính sang tháng sau') : 'Lá Bồ Đề được cộng sau khi được duyệt (demo: ~4 giây)'}

{deedList}
); } return (
{/* trần tháng còn lại — Phòng Ác */}

{remaining === 0 ? 'Đã chạm trần 500 Lá/tháng — Lá mới không cộng thêm (Phòng Ác)' : Còn {remaining} Lá trong trần {MONTH_CAP} Lá/tháng (Phòng Ác)}

{/* 11 loại thiện hành (khớp 11 lĩnh vực hệ thống) */}
{DEED_TYPES.map((t, i) => ( setSel(i)} style={{ display: 'flex', alignItems: 'center', gap: 13, padding: 15, borderColor: sel === i ? C.saffron : C.line, background: sel === i ? '#fff6e6' : C.card, }}>

{t[1]}

{t[2]}

+{t[3]} {sel===i && }
))}
{/* đơn vị xác nhận */}

Đơn vị xác nhận

Mọi thiện hành cần được xác nhận trước khi cộng Lá Bồ Đề

{approvers.map(a => ( setApprover(a.key)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 14, cursor: 'pointer', borderColor: approver === a.key ? C.saffron : C.line, background: approver === a.key ? '#fff6e6' : C.card }}>

{a.t}

{a.d}

{approver===a.key && }
))}
{/* minh chứng — demo (Webapp có trạng thái "Chờ minh chứng") */}

Đính kèm minh chứng

Ảnh/video minh chứng giúp chùa xác thực nhanh hơn

setEvidence(e => !e)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 14, cursor: 'pointer', borderColor: evidence ? C.saffron : C.line, background: evidence ? '#fff6e6' : C.card }}>

{evidence ? 'Đã đính kèm minh chứng' : 'Chụp ảnh / chọn từ thư viện'}

{evidence ? 'Chạm để gỡ tệp đính kèm' : 'Ảnh hoặc video về thiện hành của bạn'}

{evidence ? demo_minhchung.jpg ✓ : }
{deedList}
); } // ════════ GALA ════════ function GalaScreen({ nav }) { // Top-3 chuẩn (đồng bộ Portal & Webapp): Minh Tuệ #1 · Diệu Tâm #2 · Nguyên Hạnh #3 // Bình chọn nối vào store — phiếu persist; 1 phiếu/người, đổi ứng viên = chuyển phiếu. const s = useStore(); const medalColor = ['#f5b800', '#bcae97', '#d08a3e']; const voted = s.voted, votes = s.votes; return (
nav && nav('home')} />
{GALA_TOP.map((p, i) => (
{p[2]} {p[0]} {p[1]}
{votes[i]} phiếu bình chọn
))}
{/* phiếu của bạn — tóm tắt từ store */}

{voted != null ? `Phiếu của bạn: ${GALA_TOP[voted][0]}` : 'Bạn chưa bình chọn — chọn 1 trong Top 3 phía trên'}

Mỗi Phật tử 1 phiếu / hạng mục — demo. Bình chọn ứng viên khác sẽ chuyển phiếu của bạn.

{GALA_REST.map((p, i) => ( {i + 4} {p[0]} {p[1]} ))} {/* vị trí của bạn — ghim cuối bảng, hạng #214 GHIM demo (đồng bộ Trang chủ & Hồ sơ) */} #{s.rank} Bạn — {s.dharma} {fmt(s.leaves)}

Hạng #214 được ghim cố định trong demo — không đổi khi tương tác.

); } // ════════ PROFILE ════════ // Huy hiệu ĐỘNG — unlock theo điều kiện đọc từ store; khóa = mờ + điều kiện. const BADGE_DEFS = [ { ic: 'temple', t: 'Bước chân đầu', req: 'Check-in 1 chùa', cond: s => s.checkins.length >= 1 }, { ic: 'check', t: 'Tam thiện hạnh', req: '3 thiện hành được duyệt', cond: s => approvedDeeds(s) >= 3 }, { ic: 'connect', t: 'Người gieo duyên',req: 'Hoàn thành 1 mốc dự án', cond: s => anyMilestoneDone(s) }, { ic: 'award', t: 'Một phiếu tín tâm',req: 'Bình chọn tại Gala', cond: s => s.voted != null }, { ic: 'leaf', t: 'Cây tỏa bóng', req: 'Chạm cấp cây 5 (2.000 Lá)', cond: s => treeStage(s.leaves) >= 4 }, { ic: 'star', t: 'Top 8%', req: '', cond: () => true }, ]; function ProfileScreen({ nav }) { const s = useStore(); const [cover, setCover] = useState(() => { try { return localStorage.getItem('sdtd_cover') || ''; } catch(e){ return ''; } }); const [avatar, setAvatar] = useState(() => { try { return localStorage.getItem('sdtd_avatar') || ''; } catch(e){ return ''; } }); const [editing, setEditing] = useState(false); const [nm, setNm] = useState(''); const [dh, setDh] = useState(''); const [confirmReset, setConfirmReset] = useState(false); const timers = useRef([]); const mounted = useRef(true); useEffect(() => () => { mounted.current = false; timers.current.forEach(clearTimeout); }, []); // đồng bộ ảnh bìa/avatar với localStorage mỗi khi store đổi — để resetDemo() // (đã removeItem 'sdtd_cover'/'sdtd_avatar') xóa được ảnh đang hiển thị. useEffect(() => { try { setCover(localStorage.getItem('sdtd_cover') || ''); setAvatar(localStorage.getItem('sdtd_avatar') || ''); } catch (e) {} }, [s]); const later = (fn, ms) => { timers.current.push(setTimeout(fn, ms)); }; function pick(setter, key) { const inp = document.createElement('input'); inp.type = 'file'; inp.accept = 'image/*'; inp.onchange = () => { const f = inp.files[0]; if (!f) return; const r = new FileReader(); // lưu localStorage vô điều kiện; setState chỉ khi còn mounted (đổi tab giữa chừng) r.onload = () => { try { localStorage.setItem(key, r.result); } catch(e){} if (mounted.current) setter(r.result); }; r.readAsDataURL(f); }; inp.click(); } function startEdit() { setNm(s.name); setDh(s.dharma); setEditing(true); } function askReset() { if (confirmReset) { setConfirmReset(false); setEditing(false); actions.resetDemo(); return; } setConfirmReset(true); later(() => setConfirmReset(false), 3000); } const inpStyle = { width: '100%', boxSizing: 'border-box', border: `1px solid ${C.line}`, borderRadius: 12, padding: '10px 13px', fontFamily: F, fontSize: 13.5, color: C.ink, background: C.card, outline: 'none' }; const monthPct = Math.min(1, s.monthLeaves / MONTH_CAP); return (
{/* cover */}
pick(setCover, 'sdtd_cover')} style={{ height: 150, paddingTop: 0, cursor: 'pointer', background: cover ? `center/cover no-repeat url(${cover})` : 'linear-gradient(165deg,#ffd35e 0%, #f59000 60%, #e07700 100%)', borderRadius: '0 0 24px 24px', position: 'relative' }}> Ảnh bìa
{/* avatar + name (sửa được — lưu store) */}
pick(setAvatar, 'sdtd_avatar')} style={{ width: 92, height: 92, borderRadius: 100, margin: '0 auto', border: '4px solid #fff', position: 'relative', cursor: 'pointer', overflow: 'hidden', background: avatar ? `center/cover no-repeat url(${avatar})` : 'linear-gradient(150deg,#ffd35e,#f59000)', boxShadow: '0 6px 18px rgba(245,144,0,0.25)' }}> {!avatar &&
}
{editing ? (
setDh(e.target.value)} placeholder="Pháp danh" style={inpStyle}/> setNm(e.target.value)} placeholder="Họ tên" style={inpStyle}/>
) : (

{s.dharma} Sửa

{s.name} · Phật tử · Tham gia từ 05/2026

)}
{[[fmt(s.leaves), 'Lá Bồ Đề'], [String(18 + approvedDeeds(s)), 'Thiện hành'], ['#' + s.rank, 'Xếp hạng']].map(st2 => (

{st2[0]}

{st2[1]}

))}
{/* trần Lá/tháng — cơ chế Phòng Ác (live từ store, khớp Trang chủ) */}
{s.monthLeaves} / {MONTH_CAP} Lá tháng này {Math.round(monthPct * 100)}%

Trần {MONTH_CAP} Lá/tháng — cơ chế Phòng Ác

Chùa đã check-in

nav && nav('checkin')} style={{ color: C.saffronD, fontSize: 13, fontWeight: 700, cursor: 'pointer' }}>+ Check-in ›
{s.checkins.map(c => ( nav && nav('checkin')} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 14, cursor: 'pointer' }}>

{c.name}

{c.place} · {c.date}

Đã đến
))}
{/* thành tựu — huy hiệu động, khóa hiện mờ + điều kiện */}

Thành tựu tu tập

{BADGE_DEFS.map(b => { const on = b.cond(s); return (

{b.t}

{!on &&

{b.req}

}
); })}
{/* lịch sử Lá Bồ Đề — từ store */}

Lịch sử Lá Bồ Đề

{s.history.slice(0, 6).map(a => (

{a.t}

{a.date}

+{a.leaf}
))}

Hồ sơ công khai

sangdaotrongdoi.net/u/minhan

{/* đặt lại dữ liệu demo — confirm 2 chạm */}
); } // ════════ CẦU NỐI (Connect) ════════ // 3 dự án từ store với mốc tuần tự: Tham gia → mốc 1 mở → hoàn thành mốc // (+15–50 Lá theo từng mốc, đồng bộ Webapp; confirm 2 chạm) → mở mốc kế → đủ mốc = hoàn thành. function ConnectScreen() { const s = useStore(); const [cat, setCat] = useState('Tất cả'); const [confirmId, setConfirmId] = useState(null); // confirm nhẹ cho "Hoàn thành mốc" const timers = useRef([]); useEffect(() => () => { timers.current.forEach(clearTimeout); }, []); const later = (fn, ms) => { timers.current.push(setTimeout(fn, ms)); }; function askComplete(pid) { if (confirmId === pid) { setConfirmId(null); actions.completeMilestone(pid); return; } setConfirmId(pid); later(() => setConfirmId(c => c === pid ? null : c), 2600); } // chip lọc suy ra từ thực tế dữ liệu — phủ cả cat của việc nhận từ check-in chùa // ('Tu học', 'Thiện nguyện', 'Di sản', 'Truyền thông', 'Cộng đồng'…), không hardcode 3 nhóm. const cats = ['Tất cả', ...new Set([...s.connectTasks, ...s.projects].map(x => x.cat))]; const tasks = s.connectTasks.filter(p => cat === 'Tất cả' || p.cat === cat); const projects = s.projects.filter(p => cat === 'Tất cả' || p.cat === cat); const total = tasks.length + projects.length; return (

Cầu Nối là gì?

Nơi chùa & đạo tràng đăng dự án thiện nguyện đang cần người chung tay. Bạn “gieo duyên” tham gia, hoàn thành từng mốc và được ghi nhận +15–50 Lá Bồ Đề mỗi mốc.

{cats.map(c => ( ))}

{total} dự án cần hỗ trợ

{/* việc nhận từ check-in chùa */} {tasks.map(p => (

{p.t}

{p.org}

Đã nhận từ check-in
{p.need}{p.due ? ` · hạn ${p.due}` : ''} ~{p.leaf}
))} {/* dự án Cầu Nối — vòng đời đầy đủ */} {projects.map(p => { const doneCount = p.milestones.filter(m => m.done).length; const openIdx = p.milestones.findIndex(m => !m.done); return (

{p.t}

{p.org}

{p.cat} {p.completed && Dự án hoàn thành} {p.joined && !p.completed && ● Đang tham gia · {doneCount}/{p.milestones.length} mốc}
{!p.joined && !p.completed && (
{p.need} ~{p.milestones.reduce((a, m) => a + m.leaf, 0)}
)} {/* panel mốc — mốc 1 mở, còn lại khóa tuần tự */} {(p.joined || p.completed) && (
{p.milestones.map((m, i) => { const stt = m.done ? 'done' : (p.joined && i === openIdx ? 'open' : 'locked'); return (
{stt === 'done' ? : stt === 'locked' ? : i + 1} {m.t} {stt === 'open' && ( )} {stt === 'done' && +{m.leaf}}
); })} {p.joined && !p.completed && ( actions.leaveProject(p.id)} style={{ alignSelf: 'flex-end', color: C.faint, fontSize: 11.5, fontWeight: 700, cursor: 'pointer', textDecoration: 'underline' }}>Rời dự án )} {p.completed && (

Bạn đã đi trọn {p.milestones.length} mốc của dự án này. Lành thay!

)}
)}
); })}
); } // ════════ THÔNG BÁO (Notifications) ════════ // Render từ store (mới nhất trên đầu) — tap để đánh dấu đã đọc, nút "Đọc tất cả". function NotificationScreen() { const s = useStore(); const unread = unreadCount(s); const groups = [ { day: 'Hôm nay', items: s.notifications.filter(n => n.day === 'Hôm nay') }, { day: 'Trước đó', items: s.notifications.filter(n => n.day !== 'Hôm nay') }, ].filter(g => g.items.length > 0); const toneColor = { ok: '#3f8a3f', gold: C.saffronD }; const toneBg = { ok: 'rgba(63,138,63,0.12)', gold: 'rgba(245,144,0,0.12)' }; return (
{unread > 0 ? `${unread} thông báo chưa đọc` : 'Bạn đã đọc tất cả'}
{groups.map(g => (

{g.day}

{g.items.map(n => ( n.unread && actions.markRead(n.id)} style={{ display: 'flex', gap: 12, padding: 14, borderColor: n.unread ? 'rgba(245,144,0,0.3)' : C.line, background: n.unread ? '#fffaf0' : C.card, cursor: n.unread ? 'pointer' : 'default' }}>

{n.t}

{n.d}

{n.time} {n.unread && }
))}
))}
); } // ════════ THIỆN TRI THỨC (Chat) ════════ // Demo không backend. Input TỰ DO → keyword matching (mượn matchFlow của // thien-tri-thuc.html). Trích dẫn kinh văn dùng PLACEHOLDER (chờ Ban Văn hóa // kiểm định bản dịch) — tuyệt đối không chèn bản dịch kinh thật vào demo. function CiteCard({ src, tags }) { return (

[Trích dẫn kinh văn — chờ Ban Văn hóa kiểm định]

{src}

{tags.map(t => {t})}
); } function ChatScreen({ nav }) { const s = useStore(); const [text, setText] = useState(''); const askedQs = s.chatLog.map(e => e.q); const remaining = CHAT_FLOWS.map(f => f.q).filter(q => !askedQs.includes(q)); function send() { const t = text.trim(); if (!t) return; actions.askChat(t); setText(''); } return (
{/* header */}
🪷

Thiện Tri Thức

Không trả lời thay — chỉ gửi trích dẫn nguyên văn kèm nguồn

{/* messages — log persist trong store */}
{s.chatLog.map(entry => { const flow = entry.flow >= 0 ? CHAT_FLOWS[entry.flow] : null; const cites = flow ? flow.cites : CHAT_FALLBACK.cites; const ctx = flow ? flow.ctx : CHAT_FALLBACK.ctx; return ( {/* user bubble */}
{entry.q}
{/* bot bubble */}

Bạn đang chạm tới chủ đề {ctx}. Trong kho kinh sách đã kiểm định có {cites.length} trích dẫn liên quan đến điều bạn chia sẻ:

{cites.map(c => )}

Mời bạn tự đọc và chiêm nghiệm

{remaining.length > 0 && }
); })}
{/* suggestion chips + input bar (gõ tự do → keyword matching) */}
{remaining.length > 0 && (
{remaining.map(q => ( ))}
)}
setText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') send(); }} placeholder="Chia sẻ điều bạn đang trăn trở…" style={{ flex: 1, minWidth: 0, background: C.card, border: `1px solid ${C.line}`, borderRadius: 100, padding: '12px 17px', color: C.ink, fontSize: 13, fontFamily: F, outline: 'none' }}/>
); } // ════════ TAB BAR ════════ // LƯU Ý ĐIỀU HƯỚNG (đặc tả Đợt 2 — mục 5.4): KHÔNG thêm tab thứ 6 cho Thiện Tri Thức. // TabBar giữ nguyên 5 vị trí (4 tab + nút Ghi nhận ở giữa); ChatScreen chỉ được mở // từ card "🪷 Thiện Tri Thức" trên Trang chủ (HomeScreen). // Badge thông báo: subscribe store — hiện SỐ chưa đọc, ẩn khi 0. function TabBar({ tab, setTab }) { const s = useStore(); const unread = unreadCount(s); const tabs = [['home','Trang chủ','home'],['connect','Cầu Nối','connect'],['record','',''],['notifications','Thông báo','bell'],['profile','Hồ sơ','person']]; return (
{tabs.map((t) => { if (t[0] === 'record') return ( ); const active = tab === t[0]; return ( ); })}
); } Object.assign(window, { HomeScreen, CheckInScreen, RecordScreen, GalaScreen, ProfileScreen, ConnectScreen, NotificationScreen, ChatScreen, TabBar, Icon });