added bg feature

This commit is contained in:
2026-04-13 14:49:12 +05:30
parent 34254f94f9
commit 1353dfc69d
9 changed files with 750 additions and 16 deletions

View File

@@ -1,6 +1,7 @@
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'
@@ -13,6 +14,7 @@ import {
} from '../hooks/useReminder'
const MAX_PHOTO_SIZE = 200 // px — resize to 200x200
const MAX_BG_HISTORY = 3
function resizeImage(file: File): Promise<string> {
return new Promise((resolve, reject) => {
@@ -73,6 +75,16 @@ export default function SettingsPage() {
const [editPhotoPreview, setEditPhotoPreview] = useState<string | null>(null)
const [saving, setSaving] = useState(false)
// Background image state
const bgFileInputRef = useRef<HTMLInputElement>(null)
const [showBgModal, setShowBgModal] = useState(false)
const [cropperSrc, setCropperSrc] = useState<string | null>(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
// Continue onboarding tour if navigated here from the history page tour
useEffect(() => {
if (hasPendingTourStep() === 'settings') {
@@ -136,6 +148,52 @@ export default function SettingsPage() {
}
}
async function bgUpdate(updates: Parameters<typeof updateUserProfile>[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 handleBgFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
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)
@@ -443,6 +501,27 @@ export default function SettingsPage() {
<div className="settings-divider"></div>
*/}
<button type="button" className="settings-item settings-item-button" onClick={() => setShowBgModal(true)}>
<div className="settings-item-icon settings-item-icon-blue">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
</div>
<div className="settings-item-content">
<h4 className="settings-item-title">Background</h4>
<p className="settings-item-subtitle">
{activeImage ? 'Custom image active' : bgImages.length > 0 ? `${bgImages.length} saved` : 'Default color'}
</p>
</div>
<svg className="settings-item-arrow" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
<div className="settings-divider"></div>
<div id="tour-theme-switcher" className="settings-item">
<div className="settings-item-icon settings-item-icon-blue">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
@@ -718,6 +797,110 @@ export default function SettingsPage() {
</div>
)}
{/* Background Image Gallery Modal */}
{showBgModal && (
<div className="confirm-modal-overlay" onClick={() => !bgApplying && setShowBgModal(false)}>
<div className="bg-modal" onClick={(e) => e.stopPropagation()}>
<h3 className="edit-modal-title" style={{ marginBottom: '0.25rem' }}>Background</h3>
<p className="settings-item-subtitle" style={{ marginBottom: '1rem' }}>
Tap to apply · + to upload new
</p>
{/* Hidden file input */}
<input
ref={bgFileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleBgFileSelect}
/>
{/* Gallery row */}
<div className="bg-gallery">
{/* Default color swatch — always first */}
<button
type="button"
className={`bg-gallery-swatch${!activeImage ? ' bg-gallery-item--active' : ''}`}
onClick={handleApplyDefault}
disabled={bgApplying}
title="Default color"
>
<div className="bg-gallery-swatch-fill" />
{!activeImage && (
<div className="bg-gallery-badge">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
)}
<span className="bg-gallery-label">Default</span>
</button>
{/* History thumbnails */}
{bgImages.map((img, i) => (
<button
key={i}
type="button"
className={`bg-gallery-thumb${img === activeImage ? ' bg-gallery-item--active' : ''}`}
onClick={() => handleApplyFromGallery(img)}
disabled={bgApplying}
title={`Background ${i + 1}`}
>
<img src={img} alt="" className="bg-gallery-thumb-img" />
{img === activeImage && (
<div className="bg-gallery-badge">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
)}
</button>
))}
{/* Add new */}
<button
type="button"
className="bg-gallery-add"
onClick={() => bgFileInputRef.current?.click()}
disabled={bgApplying}
title="Upload new image"
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
<span className="bg-gallery-label">New</span>
</button>
</div>
{bgApplying && (
<p className="settings-item-subtitle" style={{ textAlign: 'center', marginTop: '0.75rem' }}>
Saving
</p>
)}
<button
type="button"
className="bg-close-btn"
onClick={() => setShowBgModal(false)}
disabled={bgApplying}
>
Close
</button>
</div>
</div>
)}
{/* Fullscreen image cropper */}
{cropperSrc && (
<BgImageCropper
imageSrc={cropperSrc}
aspectRatio={window.innerWidth / window.innerHeight}
onCrop={handleCropDone}
onCancel={handleCropCancel}
/>
)}
{/* Daily Reminder Modal */}
{showReminderModal && (
<div className="confirm-modal-overlay" onClick={() => !reminderSaving && setShowReminderModal(false)}>