import { useState, useEffect, useCallback, useRef } from 'react' import { useAuth } from '../contexts/AuthContext' import { deleteUser as deleteUserApi, updateUserProfile } from '../lib/api' import { BgImageCropper } from '../components/BgImageCropper' import { clearDeviceKey, clearEncryptedSecretKey } from '../lib/crypto' import { useNavigate } from 'react-router-dom' import BottomNav from '../components/BottomNav' import { useOnboardingTour, hasPendingTourStep, clearPendingTourStep } from '../hooks/useOnboardingTour' import { PageLoader } from '../components/PageLoader' import { usePWAInstall } from '../hooks/usePWAInstall' import { getSavedReminderTime, isReminderEnabled, enableReminder, disableReminder, reenableReminder, } from '../hooks/useReminder' import ClockTimePicker from '../components/ClockTimePicker' const MAX_PHOTO_SIZE = 200 // px — resize to 200x200 const MAX_BG_HISTORY = 3 function resizeImage(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => { const img = new Image() img.onload = () => { const canvas = document.createElement('canvas') canvas.width = MAX_PHOTO_SIZE canvas.height = MAX_PHOTO_SIZE const ctx = canvas.getContext('2d')! // Crop to square from center const side = Math.min(img.width, img.height) const sx = (img.width - side) / 2 const sy = (img.height - side) / 2 ctx.drawImage(img, sx, sy, side, side, 0, 0, MAX_PHOTO_SIZE, MAX_PHOTO_SIZE) resolve(canvas.toDataURL('image/jpeg', 0.8)) } img.onerror = reject img.src = reader.result as string } reader.onerror = reject reader.readAsDataURL(file) }) } export default function SettingsPage() { const { user, userId, mongoUser, signOut, loading, refreshMongoUser } = useAuth() // const [passcodeEnabled, setPasscodeEnabled] = useState(false) // Passcode lock — disabled for now // const [faceIdEnabled, setFaceIdEnabled] = useState(false) // Face ID — disabled for now const [theme, setTheme] = useState<'light' | 'dark'>(() => { return (localStorage.getItem('gj-theme') as 'light' | 'dark') || 'light' }) const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) // Clear Data confirmation modal state const [showClearModal, setShowClearModal] = useState(false) const [confirmEmail, setConfirmEmail] = useState('') const [deleting, setDeleting] = useState(false) const { continueTourOnSettings } = useOnboardingTour() const navigate = useNavigate() const fileInputRef = useRef(null) const { canInstall, isIOS, triggerInstall } = usePWAInstall() const [installModal, setInstallModal] = useState<'ios' | 'chrome' | null>(null) // Reminder state const [reminderTime, setReminderTime] = useState(() => getSavedReminderTime()) const [reminderEnabled, setReminderEnabled] = useState(() => isReminderEnabled()) const [showReminderModal, setShowReminderModal] = useState(false) const [reminderPickedTime, setReminderPickedTime] = useState('08:00') const [reminderError, setReminderError] = useState(null) const [reminderSaving, setReminderSaving] = useState(false) // Edit profile modal state const [showEditModal, setShowEditModal] = useState(false) const [editName, setEditName] = useState('') const [editPhotoPreview, setEditPhotoPreview] = useState(null) const [saving, setSaving] = useState(false) // Background image state const bgFileInputRef = useRef(null) const [showBgModal, setShowBgModal] = useState(false) const [cropperSrc, setCropperSrc] = useState(null) const [bgApplying, setBgApplying] = useState(false) // Derived from mongoUser (no local state — always fresh after refreshMongoUser) const bgImages: string[] = (mongoUser as { backgroundImages?: string[] } | null)?.backgroundImages ?? [] const activeImage: string | null = mongoUser?.backgroundImage ?? null // Tile aspect ratio matches the actual screen so previews reflect real proportions const screenAspect = `${window.innerWidth} / ${window.innerHeight}` // Continue onboarding tour if navigated here from the history page tour useEffect(() => { if (hasPendingTourStep() === 'settings') { clearPendingTourStep() continueTourOnSettings() } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const handleSeeTutorial = () => { localStorage.setItem('gj-force-tour', 'true') navigate('/write') } const displayName = mongoUser?.displayName || user?.displayName || 'User' // Use custom uploaded photo (base64) if set, otherwise always use Firebase's fresh Google URL const mongoPhoto = mongoUser && 'photoURL' in mongoUser ? mongoUser.photoURL : null const photoURL = (mongoPhoto?.startsWith('data:')) ? mongoPhoto : (user?.photoURL || null) const openEditModal = () => { setEditName(displayName) setEditPhotoPreview(photoURL) setShowEditModal(true) } const handlePhotoSelect = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return try { const resized = await resizeImage(file) setEditPhotoPreview(resized) } catch { setMessage({ type: 'error', text: 'Failed to process image' }) } } const handleSaveProfile = async () => { if (!user || !userId) return setSaving(true) try { const token = await user.getIdToken() const updates: { displayName?: string; photoURL?: string } = {} if (editName.trim() && editName.trim() !== displayName) { updates.displayName = editName.trim() } if (editPhotoPreview !== photoURL) { updates.photoURL = editPhotoPreview || '' } if (Object.keys(updates).length > 0) { await updateUserProfile(userId, updates, token) await refreshMongoUser() setMessage({ type: 'success', text: 'Profile updated!' }) setTimeout(() => setMessage(null), 2000) } setShowEditModal(false) } catch (error) { const msg = error instanceof Error ? error.message : 'Failed to update profile' setMessage({ type: 'error', text: msg }) } finally { setSaving(false) } } async function bgUpdate(updates: Parameters[1]) { if (!user || !userId) return setBgApplying(true) try { const token = await user.getIdToken() await updateUserProfile(userId, updates, token) await refreshMongoUser() } catch (error) { const msg = error instanceof Error ? error.message : 'Failed to update background' setMessage({ type: 'error', text: msg }) } finally { setBgApplying(false) } } const handleApplyDefault = () => { if (!activeImage) return // already on default bgUpdate({ backgroundImage: null }) } const handleApplyFromGallery = (img: string) => { if (img === activeImage) return // already active bgUpdate({ backgroundImage: img }) } const handleDeleteBgImage = (img: string, e: React.MouseEvent) => { e.stopPropagation() const newHistory = bgImages.filter(i => i !== img) // If the deleted image was active, clear it too const updates: Parameters[1] = { backgroundImages: newHistory } if (img === activeImage) updates.backgroundImage = null bgUpdate(updates) } const handleBgFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return setShowBgModal(false) setCropperSrc(URL.createObjectURL(file)) e.target.value = '' } const handleCropDone = async (dataUrl: string) => { if (cropperSrc) URL.revokeObjectURL(cropperSrc) setCropperSrc(null) // Prepend to history, deduplicate, cap at MAX_BG_HISTORY const newHistory = [dataUrl, ...bgImages.filter(i => i !== dataUrl)].slice(0, MAX_BG_HISTORY) await bgUpdate({ backgroundImage: dataUrl, backgroundImages: newHistory }) } const handleCropCancel = () => { if (cropperSrc) URL.revokeObjectURL(cropperSrc) setCropperSrc(null) } // Apply theme to DOM const applyTheme = useCallback((t: 'light' | 'dark') => { document.documentElement.setAttribute('data-theme', t) localStorage.setItem('gj-theme', t) }, []) // Apply saved theme on mount useEffect(() => { applyTheme(theme) }, [theme, applyTheme]) const handleThemeChange = (newTheme: 'light' | 'dark') => { setTheme(newTheme) applyTheme(newTheme) setMessage({ type: 'success', text: `Switched to ${newTheme === 'light' ? 'Light' : 'Dark'} theme` }) setTimeout(() => setMessage(null), 2000) } const handleClearData = () => { setConfirmEmail('') setShowClearModal(true) } const handleConfirmDelete = async () => { if (!user || !userId) return const userEmail = user.email || '' if (confirmEmail.trim().toLowerCase() !== userEmail.toLowerCase()) { setMessage({ type: 'error', text: 'Email does not match. Please try again.' }) return } setDeleting(true) setMessage(null) try { const token = await user.getIdToken() // Delete user and all entries from backend await deleteUserApi(userId, token) // Clear all local crypto data clearDeviceKey() await clearEncryptedSecretKey() localStorage.removeItem('gj-kdf-salt') localStorage.removeItem('gj-theme') setShowClearModal(false) // Sign out (clears auth state) await signOut() } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to delete account' setMessage({ type: 'error', text: errorMessage }) setDeleting(false) } } const handleOpenReminderModal = () => { setReminderPickedTime(reminderTime || '08:00') setReminderError(null) setShowReminderModal(true) } const handleSaveReminder = async () => { if (!user || !userId) return setReminderSaving(true) setReminderError(null) const authToken = await user.getIdToken() const error = await enableReminder(reminderPickedTime, userId, authToken) setReminderSaving(false) if (error) { setReminderError(error) } else { setReminderTime(reminderPickedTime) setReminderEnabled(true) setShowReminderModal(false) setMessage({ type: 'success', text: 'Reminder set!' }) setTimeout(() => setMessage(null), 2000) } } const handleReminderToggle = async () => { if (!user || !userId) return if (!reminderTime) { handleOpenReminderModal() return } if (reminderEnabled) { const authToken = await user.getIdToken() await disableReminder(userId, authToken) setReminderEnabled(false) } else { setReminderSaving(true) const authToken = await user.getIdToken() const error = await reenableReminder(userId, authToken) setReminderSaving(false) if (error) { setReminderError(error) setShowReminderModal(true) } else { setReminderEnabled(true) setMessage({ type: 'success', text: 'Reminder enabled!' }) setTimeout(() => setMessage(null), 2000) } } } const handleSignOut = async () => { try { await signOut() } catch (error) { console.error('Error signing out:', error) } } if (loading) { return } return (

Settings

Manage your privacy and preferences.

{/* Profile Section */}
{photoURL ? ( {displayName} { (e.target as HTMLImageElement).style.display = 'none'; }} /> ) : (
{displayName.charAt(0).toUpperCase()}
)}

{displayName}

{/* Privacy & Security */}

PRIVACY & SECURITY

{/* Passcode Lock — disabled for now, toggle is non-functional */}

Passcode Lock

Coming soon

{/* Face ID — commented out for future use */} {/*

Face ID

Unlock with glance

*/}
{/* App */}

APP

{/* Daily Reminder */}
{/* Data & Look */}

DATA & LOOK

{/* Export Journal — commented out for future use */} {/*
*/}

Theme

Currently: {theme === 'light' ? 'Light' : 'Dark'}

{message && (
{message.text}
)} {/* See Tutorial */} {/* Clear Data */} {/* Sign Out */} {/* Version */} {/*

VERSION 1.0.2

*/}

End-to-end Encrypted

{/* Clear Data Confirmation Modal */} {showClearModal && (
!deleting && setShowClearModal(false)}>
e.stopPropagation()}>
⚠️

Delete All Data?

This will permanently delete your account, all journal entries, and local encryption keys. This action cannot be undone.

Type your email to confirm:

setConfirmEmail(e.target.value)} disabled={deleting} autoFocus />
)} {/* Edit Profile Modal */} {showEditModal && (
!saving && setShowEditModal(false)}>
e.stopPropagation()}>

Edit Profile

{editPhotoPreview && ( )} setEditName(e.target.value)} disabled={saving} maxLength={50} autoFocus onFocus={(e) => (e.target.style.borderColor = 'var(--color-primary)')} onBlur={(e) => (e.target.style.borderColor = '')} />
)} {/* Add to Home Screen instructions modal */} {installModal && (
setInstallModal(null)}>
e.stopPropagation()}>
🌱

Add to Home Screen

{installModal === 'ios' ? ( <>

Follow these steps in Safari:

  1. Tap the Share button at the bottom of Safari
  2. Scroll down and tap Add to Home Screen
  3. Tap Add to confirm
) : ( <>

Follow these steps in Chrome:

  1. Tap the ⋮ menu in the top-right corner
  2. Tap Add to Home screen
  3. Tap Add to confirm
)}
)} {/* Background Image Gallery Modal */} {showBgModal && (
!bgApplying && setShowBgModal(false)}>
e.stopPropagation()}>

Background

Add new images or select from previously used ones:

{/* Hidden file input */} {/* Fixed 4-tile grid: [+] [slot1] [slot2] [slot3] */}
{/* Add new — always first tile */} {/* 3 image slots — filled or empty placeholder */} {Array.from({ length: MAX_BG_HISTORY }).map((_, i) => { const img = bgImages[i] if (img) { return (
) } return (
) })}
{/* Revert to default — only shown when a custom bg is active */} {activeImage && ( )} {bgApplying && (

Saving…

)}
)} {/* Fullscreen image cropper */} {cropperSrc && ( )} {/* Daily Reminder Modal */} {showReminderModal && (
!reminderSaving && setShowReminderModal(false)}>
e.stopPropagation()}>
🔔

{reminderTime ? 'Edit Reminder' : 'Set Daily Reminder'}

{reminderError && (

{reminderError}

)}
)}
) }