import { useAuth, useCollection } from '@deplixo/sdk'; /* @component-map * App — Main container, tab navigation [app.jsx] * ContactList — Contact list sorted by days since last contact [components/ContactList.jsx] * ContactForm — Add/edit contact form modal [components/ContactForm.jsx] * ContactDetail — Contact detail view modal [components/ContactDetail.jsx] * Dashboard — Overview stats and quick insights [components/Dashboard.jsx] * GoogleSignInGate — Google OAuth gate that blocks the app until the user signs in, and provides a signed-in user bar with sign out. * @end-component-map */ // DEPLOY_CONFIG: {"cron": [{"name": "daily_reminder_trigger", "schedule": "0 9 * * *", "action": "event", "config": {"event_type": "send_daily_reminders"}}], "triggers": [{"name": "reminder_state_upsert", "on": "collection.add", "collection": "reminder_state", "actions": [{"type": "email", "to": "{{recipient_email}}", "subject": "Reminder: follow up with your contacts", "body": "You have contacts that have not been contacted in over 30 days. Please review and send a follow-up."}]}]} import { useMemo, useState } from 'react'; import { Dashboard } from './components/Dashboard.jsx'; import { ContactList } from './components/ContactList.jsx'; import { ContactForm } from './components/ContactForm.jsx'; import { ContactDetail } from './components/ContactDetail.jsx'; const INTERACTION_TYPES = [ { id: 'meeting', label: 'Meeting', icon: '🤝' }, { id: 'call', label: 'Call', icon: '📞' }, { id: 'email', label: 'Email', icon: '✉️' }, ]; function App() { const [activeTab, setActiveTab] = useState('contacts'); const [showForm, setShowForm] = useState(false); const [editingContact, setEditingContact] = useState(null); const [viewingContact, setViewingContact] = useState(null); const { user, loading, login, logout } = useAuth(); const [authError, setAuthError] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const [companyFilter, setCompanyFilter] = useState('all'); const [roleFilter, setRoleFilter] = useState('all'); const [tagFilter, setTagFilter] = useState('all'); const contactsStore = useCollection('contacts', { personal: true }); const meetingStore = useCollection('contact_interactions_meeting', { personal: true }); const callStore = useCollection('contact_interactions_call', { personal: true }); const emailStore = useCollection('contact_interactions_email', { personal: true }); const interactionStores = useMemo(() => ({ meeting: meetingStore, call: callStore, email: emailStore, }), [meetingStore, callStore, emailStore]); const contactItems = contactsStore.items || []; const filterOptions = useMemo(() => { const companies = [...new Set(contactItems.map((c) => (c.company || '').trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b)); const roles = [...new Set(contactItems.map((c) => (c.role || '').trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b)); const tags = [...new Set(contactItems.flatMap((c) => { const raw = Array.isArray(c.tags) ? c.tags : typeof c.tags === 'string' ? c.tags.split(',') : []; return raw.map((tag) => String(tag).trim()).filter(Boolean); }))].sort((a, b) => a.localeCompare(b)); return { companies, roles, tags }; }, [contactItems]); const contactListData = useMemo(() => { const query = searchQuery.trim().toLowerCase(); const normalizeTags = (tags) => { if (Array.isArray(tags)) return tags.map((tag) => String(tag).trim()).filter(Boolean); if (typeof tags === 'string') return tags.split(',').map((tag) => tag.trim()).filter(Boolean); return []; }; const daysSince = (contact) => { const last = contact.lastContactDate || contact.lastContactedAt || contact.lastContact || contact.updatedAt || contact.createdAt; if (!last) return Number.POSITIVE_INFINITY; const lastDate = new Date(last); if (Number.isNaN(lastDate.getTime())) return Number.POSITIVE_INFINITY; const diffMs = Date.now() - lastDate.getTime(); return Math.max(0, Math.floor(diffMs / (1000 * 60 * 60 * 24))); }; return contactItems .map((contact) => ({ ...contact, _daysSinceLastContact: daysSince(contact), _tags: normalizeTags(contact.tags), })) .filter((contact) => { const haystack = [contact.name, contact.company, contact.role, contact.email, contact.notes, ...contact._tags] .filter(Boolean) .join(' ') .toLowerCase(); const matchesSearch = !query || haystack.includes(query); const matchesCompany = companyFilter === 'all' || (contact.company || '').trim() === companyFilter; const matchesRole = roleFilter === 'all' || (contact.role || '').trim() === roleFilter; const matchesTag = tagFilter === 'all' || contact._tags.some((tag) => tag.toLowerCase() === tagFilter.toLowerCase()); return matchesSearch && matchesCompany && matchesRole && matchesTag; }) .sort((a, b) => { const dayDiff = (b._daysSinceLastContact === Number.POSITIVE_INFINITY ? 999999 : b._daysSinceLastContact) - (a._daysSinceLastContact === Number.POSITIVE_INFINITY ? 999999 : a._daysSinceLastContact); if (dayDiff !== 0) return dayDiff; return (a.name || '').localeCompare(b.name || ''); }); }, [contactItems, searchQuery, companyFilter, roleFilter, tagFilter]); const interactionItems = useMemo(() => { const all = [ ...(meetingStore.items || []).map((item) => ({ ...item, interactionType: 'meeting' })), ...(callStore.items || []).map((item) => ({ ...item, interactionType: 'call' })), ...(emailStore.items || []).map((item) => ({ ...item, interactionType: 'email' })), ]; return all.sort((a, b) => new Date(b.date || b.createdAt || 0) - new Date(a.date || a.createdAt || 0)); }, [meetingStore.items, callStore.items, emailStore.items]); const handleEdit = (contact) => { setEditingContact(contact); setShowForm(true); }; const handleCloseForm = () => { setShowForm(false); setEditingContact(null); }; const handleSignIn = async () => { try { setAuthError(''); await login('google'); } catch (error) { setAuthError('Unable to sign in with Google. Please try again.'); } }; const handleSignOut = async () => { try { setAuthError(''); await logout(); setActiveTab('contacts'); setShowForm(false); setEditingContact(null); setViewingContact(null); } catch (error) { setAuthError('Unable to sign out. Please try again.'); } }; const tabs = [ { id: 'contacts', label: 'Contacts', icon: '👥' }, { id: 'dashboard', label: 'Insights', icon: '📊' }, ]; if (loading) { return (
Loading session...
); } if (!user) { return (

Relay.

Personal CRM

Sign in with Google to access your private contacts and interactions.

{authError &&
{authError}
}
); } return (

Relay.

Personal CRM

{user.avatar && {user.name} {user.name || user.id}
{activeTab === 'contacts' && ( setShowForm(true)} contacts={contactListData} searchQuery={searchQuery} onSearchChange={setSearchQuery} companyFilter={companyFilter} onCompanyFilterChange={setCompanyFilter} roleFilter={roleFilter} onRoleFilterChange={setRoleFilter} tagFilter={tagFilter} onTagFilterChange={setTagFilter} filterOptions={filterOptions} /> )} {activeTab === 'dashboard' && }
{showForm && ( )} {viewingContact && ( setViewingContact(null)} onEdit={(c) => { setViewingContact(null); handleEdit(c); }} contactsStore={contactsStore} interactionItems={interactionItems} interactionStores={interactionStores} interactionTypes={INTERACTION_TYPES} /> )}
); } function GoogleSignInGate({ children }) { const { user, loading, login, logout } = useAuth(); const [error, setError] = useState(''); const handleLogin = async () => { try { setError(''); await login('google'); } catch (e) { setError('Google sign-in failed. Please try again.'); } }; const handleLogout = async () => { try { setError(''); await logout(); } catch (e) { setError('Unable to sign out.'); } }; if (loading) { return
Loading authentication...
; } if (!user) { return (

Relay.

Personal CRM

Sign in with Google to access your private data.

{error &&
{error}
}
); } return ( <>
{user.avatar && {user.name} {user.name || user.id}
{children} ); } ReactDOM.createRoot(document.getElementById("root")).render();