import { useState, useEffect } from 'react' import { useAuth } from '../contexts/AuthContext' import { getUserEntries, type JournalEntry } from '../lib/api' import { decryptEntry } from '../lib/crypto' import { formatIST, getISTDateComponents } from '../lib/timezone' import BottomNav from '../components/BottomNav' import { useOnboardingTour, hasPendingTourStep, clearPendingTourStep } from '../hooks/useOnboardingTour' interface DecryptedEntry extends JournalEntry { decryptedTitle?: string decryptedContent?: string decryptError?: string } export default function HistoryPage() { const { user, userId, secretKey, loading } = useAuth() const [currentMonth, setCurrentMonth] = useState(new Date()) const [selectedDate, setSelectedDate] = useState(new Date()) const [entries, setEntries] = useState([]) const [loadingEntries, setLoadingEntries] = useState(false) const [selectedEntry, setSelectedEntry] = useState(null) const { continueTourOnHistory } = useOnboardingTour() // Continue onboarding tour if navigated here from the home page tour useEffect(() => { if (hasPendingTourStep() === 'history') { clearPendingTourStep() continueTourOnHistory() } }, []) // Fetch entries on mount and when userId changes useEffect(() => { if (!user || !userId) return const fetchEntries = async () => { setLoadingEntries(true) try { const token = await user.getIdToken() const response = await getUserEntries(userId, token, 100, 0) // Decrypt entries if they are encrypted const decryptedEntries: DecryptedEntry[] = await Promise.all( response.entries.map(async (entry) => { if (entry.encryption?.encrypted && entry.encryption?.ciphertext && entry.encryption?.nonce) { // Entry is encrypted, try to decrypt if (!secretKey) { return { ...entry, decryptError: 'Encryption key not available', decryptedTitle: '[Encrypted]', } } try { const decrypted = await decryptEntry( entry.encryption.ciphertext, entry.encryption.nonce, secretKey ) // Split decrypted content: first line is title, rest is content const lines = decrypted.split('\n\n') const decryptedTitle = lines[0] const decryptedContent = lines.slice(1).join('\n\n') return { ...entry, decryptedTitle, decryptedContent, } } catch (error) { console.error(`Failed to decrypt entry ${entry.id}:`, error) return { ...entry, decryptError: 'Failed to decrypt entry', decryptedTitle: '[Decryption Failed]', } } } else { // Entry is not encrypted, use plaintext return { ...entry, decryptedTitle: entry.title || '[Untitled]', decryptedContent: entry.content || '', } } }) ) setEntries(decryptedEntries) } catch (error) { console.error('Error fetching entries:', error) } finally { setLoadingEntries(false) } } fetchEntries() }, [user, userId, secretKey]) const getDaysInMonth = (date: Date) => { const year = date.getFullYear() const month = date.getMonth() const firstDay = new Date(year, month, 1) const lastDay = new Date(year, month + 1, 0) const daysInMonth = lastDay.getDate() const startingDayOfWeek = firstDay.getDay() return { daysInMonth, startingDayOfWeek } } const hasEntryOnDate = (day: number) => { return entries.some((entry) => { const components = getISTDateComponents(entry.createdAt) return ( components.date === day && components.month === currentMonth.getMonth() && components.year === currentMonth.getFullYear() ) }) } const isToday = (day: number) => { const today = new Date() return ( day === today.getDate() && currentMonth.getMonth() === today.getMonth() && currentMonth.getFullYear() === today.getFullYear() ) } const formatDate = (date: string) => { return formatIST(date, 'date') } const formatTime = (date: string) => { return formatIST(date, 'time') } const { daysInMonth, startingDayOfWeek } = getDaysInMonth(currentMonth) const monthName = currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric', }) const previousMonth = () => { setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1)) } const nextMonth = () => { setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1)) } // Get entries for selected date (in IST) const selectedDateEntries = entries.filter((entry) => { const components = getISTDateComponents(entry.createdAt) return ( components.date === selectedDate.getDate() && components.month === selectedDate.getMonth() && components.year === selectedDate.getFullYear() ) }) const isDateSelected = (day: number) => { return ( day === selectedDate.getDate() && currentMonth.getMonth() === selectedDate.getMonth() && currentMonth.getFullYear() === selectedDate.getFullYear() ) } const handleDateClick = (day: number) => { setSelectedDate(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day)) } if (loading) { return (

Loading…

) } return (

History

Your past reflections

{/* */}

{monthName}

S
M
T
W
T
F
S
{Array.from({ length: startingDayOfWeek }).map((_, i) => (
))} {Array.from({ length: daysInMonth }).map((_, i) => { const day = i + 1 const hasEntry = hasEntryOnDate(day) const isTodayDate = isToday(day) const isSelected = isDateSelected(day) return ( ) })}

{selectedDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).toUpperCase()}

{loadingEntries ? (

Loading entries…

) : (
{selectedDateEntries.length === 0 ? (

No entries for this day yet. Start writing!

) : ( selectedDateEntries.map((entry) => ( )) )}
)}
{/* Entry Detail Modal */} {selectedEntry && (
{ if (e.target === e.currentTarget) setSelectedEntry(null) }} >
{formatDate(selectedEntry.createdAt)} {formatTime(selectedEntry.createdAt)}

{selectedEntry.decryptedTitle || selectedEntry.title || '[Untitled]'}

{selectedEntry.decryptError ? (
{selectedEntry.decryptError}
) : (
{selectedEntry.decryptedContent ? selectedEntry.decryptedContent.split('\n').map((line, i) => (

{line || '\u00A0'}

)) :

No content

}
)} {selectedEntry.encryption?.encrypted && (
End-to-end encrypted
)}
)}
) }