// DEPLOY_CONFIG: {"cron": [{"name": "auto_schedule_discussion_on_full_progress", "schedule": "*/5 * * * *", "action": "event", "config": {"event_type": "check_all_members_complete"}}], "triggers": [{"name": "send_discussion_ready_email", "on": "event", "collection": "discussion_events", "actions": [{"type": "email", "to": "{{member.email}}", "subject": "Discussion scheduled", "body": "All members have reached 100% progress. The discussion has been automatically scheduled."}]}]} import { useState, useCallback, useEffect, useMemo } from 'react'; import { useAI, useAuth, useCollection, usePresence, renderChart } from '@deplixo/sdk'; import { CurrentBook } from './components/CurrentBook.jsx'; import { ReadingNotes } from './components/ReadingNotes.jsx'; import { Discussion } from './components/Discussion.jsx'; import { BookManagement } from './components/BookManagement.jsx'; function App() { const { user, loading, logout } = useAuth(); const { generate, loading: aiLoading, error: aiError } = useAI(); const [activeTab, setActiveTab] = useState('book'); const [presencePayload, setPresencePayload] = useState({ view: 'book', chapter: null, bookTitle: null, bookId: null }); const [voteInput, setVoteInput] = useState({ title: '', author: '', genre: '' }); const [voteStatus, setVoteStatus] = useState(''); const [discussionQuestions, setDiscussionQuestions] = useState([]); const [discussionQuestionsLoading, setDiscussionQuestionsLoading] = useState(false); const [discussionQuestionsStatus, setDiscussionQuestionsStatus] = useState(''); const { items: bookItems, loading: booksLoading } = useCollection('books'); const { items: noteItems, loading: notesLoading } = useCollection('reading-notes'); const { items: discussionItems, loading: discussionLoading, add: addDiscussion, update: updateDiscussion } = useCollection('discussion'); const { items: voteItems, loading: votesLoading, add: addVote, update: updateVote, remove: removeVote } = useCollection('book-votes', { personal: true }); const currentBook = bookItems?.[0] || null; const currentChapter = presencePayload.chapter; const buildPresence = useCallback((overrides = {}) => { const book = currentBook || {}; return { userId: user?.id || user?.email || user?.name || 'unknown', name: user?.name || 'Member', avatar: user?.avatar || null, email: user?.email || null, view: activeTab, bookId: book?.id || null, bookTitle: book?.title || null, chapter: currentChapter, updatedAt: Date.now(), ...overrides }; }, [user, activeTab, currentBook, currentChapter]); const handlePresenceUpdate = useCallback((data) => { if (!data) return; setPresencePayload(prev => ({ ...prev, view: data.view ?? prev.view, chapter: data.chapter ?? prev.chapter, bookTitle: data.bookTitle ?? prev.bookTitle, bookId: data.bookId ?? prev.bookId })); }, []); const { users: presenceUsers, update: updatePresence } = usePresence(buildPresence()); const tabs = [ { id: 'book', label: 'π Current Book', icon: 'π' }, { id: 'notes', label: 'π My Notes', icon: 'π' }, { id: 'discussion', label: 'π¬ Discussion', icon: 'π¬' }, { id: 'votes', label: 'π³οΈ Book Vote', icon: 'π³οΈ' }, { id: 'manage', label: 'βοΈ Manage', icon: 'βοΈ' } ]; useEffect(() => { updatePresence(buildPresence()); }, [activeTab, currentBook, currentChapter, buildPresence, updatePresence]); useEffect(() => { if (!user) return; const defaultChapter = currentBook?.currentChapter || null; setPresencePayload(prev => ({ ...prev, view: activeTab, chapter: activeTab === 'notes' ? (prev.chapter ?? defaultChapter) : (activeTab === 'discussion' ? prev.chapter : null), bookTitle: currentBook?.title || prev.bookTitle || null, bookId: currentBook?.id || prev.bookId || null })); }, [activeTab, currentBook, user]); const normalizedVotes = useMemo(() => { const map = new Map(); (voteItems || []).forEach(vote => { if (!vote) return; const title = (vote.title || '').trim(); if (!title) return; const key = title.toLowerCase(); const existing = map.get(key) || { title, author: vote.author || '', genre: vote.genre || '', count: 0, voters: [], updatedAt: 0, ids: [] }; existing.count += Number(vote.count || 1); existing.updatedAt = Math.max(existing.updatedAt, vote.updatedAt || 0); existing.ids.push(vote.id); if (!existing.author && vote.author) existing.author = vote.author; if (!existing.genre && vote.genre) existing.genre = vote.genre; if (vote.userId) existing.voters.push(vote.userId); map.set(key, existing); }); return [...map.values()].sort((a, b) => b.count - a.count || b.updatedAt - a.updatedAt || a.title.localeCompare(b.title)); }, [voteItems]); const myVote = useMemo(() => { const userKey = user?.id || user?.email || user?.name; if (!userKey) return null; return (voteItems || []).find(vote => vote?.userId === userKey) || null; }, [voteItems, user]); const discussionBookTitle = currentBook?.title || presencePayload.bookTitle || ''; const discussionPromptCount = Array.isArray(discussionQuestions) ? discussionQuestions.length : 0; const generateDiscussionQuestions = useCallback(async () => { const title = (discussionBookTitle || '').trim(); if (!title) { setDiscussionQuestionsStatus('Add a current book title first to generate prompts.'); return; } setDiscussionQuestionsLoading(true); setDiscussionQuestionsStatus('Generating discussion prompts...'); try { const response = await generate({ system: 'You generate engaging book club discussion prompts. Return JSON only.', user: `Create 6 discussion questions for the book titled "${title}". Make them thoughtful, open-ended, and suitable for a club meeting. Return JSON in this format: {"questions":["...","..."]}`, json: true }); const questions = Array.isArray(response?.questions) ? response.questions.filter(Boolean).map(q => String(q).trim()).filter(Boolean) : []; if (questions.length === 0) { throw new Error('No questions returned'); } setDiscussionQuestions(questions); setDiscussionQuestionsStatus('Discussion prompts generated successfully.'); } catch (error) { setDiscussionQuestionsStatus('Unable to generate prompts right now.'); } finally { setDiscussionQuestionsLoading(false); } }, [generate, discussionBookTitle]); const useGeneratedQuestionsForDiscussion = useCallback(async () => { if (!discussionPromptCount) { setDiscussionQuestionsStatus('Generate discussion prompts first.'); return; } const payload = { bookId: currentBook?.id || presencePayload.bookId || null, bookTitle: discussionBookTitle || null, chapter: presencePayload.chapter || currentBook?.currentChapter || null, title: `${discussionBookTitle ? `${discussionBookTitle} ` : ''}Discussion`, questions: discussionQuestions, createdBy: user?.id || user?.email || user?.name || 'unknown', createdAt: Date.now(), updatedAt: Date.now(), status: 'scheduled' }; try { await addDiscussion(payload); setDiscussionQuestionsStatus('Scheduled discussion pre-populated with generated questions.'); } catch (error) { setDiscussionQuestionsStatus('Unable to save the scheduled discussion right now.'); } }, [discussionPromptCount, addDiscussion, currentBook, presencePayload.chapter, presencePayload.bookId, discussionBookTitle, discussionQuestions, user]); const handleVoteSubmit = async (e) => { e.preventDefault(); const title = voteInput.title.trim(); if (!title) return; const userKey = user?.id || user?.email || user?.name || 'unknown'; const existing = myVote; const payload = { userId: userKey, name: user?.name || 'Member', avatar: user?.avatar || null, email: user?.email || null, title, author: voteInput.author.trim(), genre: voteInput.genre.trim(), updatedAt: Date.now() }; try { if (existing?.id) { await updateVote(existing.id, payload); } else { await addVote({ ...payload, count: 1 }); } setVoteStatus('Your vote has been saved.'); setVoteInput({ title: '', author: '', genre: '' }); } catch (error) { setVoteStatus('Unable to save your vote right now.'); } }; const clearMyVote = async () => { if (!myVote?.id) return; try { await removeVote(myVote.id); setVoteStatus('Your vote was removed.'); } catch (error) { setVoteStatus('Unable to remove your vote right now.'); } }; if (loading) { return
Read together, grow together
{currentBook ? `${currentBook.title}${currentChapter ? ` β’ Chapter ${currentChapter}` : ''}` : 'No active book yet'}
Vote for the next read and see what the club is leaning toward.
Ranked by the number of member votes.
Sign in with Google to manage the current book and member-related actions.
{subtitle}