added bg feature
This commit is contained in:
@@ -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)}>
|
||||
|
||||
Reference in New Issue
Block a user