added image upload feature

This commit is contained in:
2026-03-16 12:10:55 +05:30
parent ef52695bd9
commit e841860bd4
4 changed files with 324 additions and 11 deletions

View File

@@ -1,13 +1,40 @@
import { useState, useEffect, useCallback } from 'react'
import { useState, useEffect, useCallback, useRef } from 'react'
import { useAuth } from '../contexts/AuthContext'
import { deleteUser as deleteUserApi } from '../lib/api'
import { deleteUser as deleteUserApi, updateUserProfile } from '../lib/api'
import { clearDeviceKey, clearEncryptedSecretKey } from '../lib/crypto'
import { useNavigate } from 'react-router-dom'
import BottomNav from '../components/BottomNav'
import { useOnboardingTour, hasPendingTourStep, clearPendingTourStep } from '../hooks/useOnboardingTour'
const MAX_PHOTO_SIZE = 200 // px — resize to 200x200
function resizeImage(file: File): Promise<string> {
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, signOut, loading } = useAuth()
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'>(() => {
@@ -22,6 +49,13 @@ export default function SettingsPage() {
const { continueTourOnSettings } = useOnboardingTour()
const navigate = useNavigate()
const fileInputRef = useRef<HTMLInputElement>(null)
// Edit profile modal state
const [showEditModal, setShowEditModal] = useState(false)
const [editName, setEditName] = useState('')
const [editPhotoPreview, setEditPhotoPreview] = useState<string | null>(null)
const [saving, setSaving] = useState(false)
// Continue onboarding tour if navigated here from the history page tour
useEffect(() => {
@@ -37,7 +71,53 @@ export default function SettingsPage() {
navigate('/')
}
const displayName = user?.displayName || 'User'
const displayName = mongoUser?.displayName || user?.displayName || 'User'
// Prefer mongo photo; only fall back to Google photo if mongo has no photo set
const photoURL = (mongoUser && 'photoURL' in mongoUser) ? (mongoUser.photoURL || null) : (user?.photoURL || null)
const openEditModal = () => {
setEditName(displayName)
setEditPhotoPreview(photoURL)
setShowEditModal(true)
}
const handlePhotoSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
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)
}
}
// Apply theme to DOM
const applyTheme = useCallback((t: 'light' | 'dark') => {
@@ -127,14 +207,23 @@ export default function SettingsPage() {
{/* Profile Section */}
<div className="settings-profile">
<div className="settings-avatar">
<div className="settings-avatar-placeholder" style={{ fontSize: '1.75rem', background: 'linear-gradient(135deg, #86efac 0%, #22c55e 100%)' }}>
🍀
</div>
{photoURL ? (
<img src={photoURL} alt={displayName} className="settings-avatar-img" />
) : (
<div className="settings-avatar-placeholder" style={{ fontSize: '1.75rem', background: 'linear-gradient(135deg, #86efac 0%, #22c55e 100%)' }}>
{displayName.charAt(0).toUpperCase()}
</div>
)}
</div>
<div className="settings-profile-info">
<h2 className="settings-profile-name">{displayName}</h2>
{/* <span className="settings-profile-badge">PRO MEMBER</span> */}
</div>
<button type="button" className="settings-edit-btn" onClick={openEditModal} title="Edit profile">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</button>
</div>
{/* Privacy & Security */}
@@ -339,6 +428,80 @@ export default function SettingsPage() {
</div>
)}
{/* Edit Profile Modal */}
{showEditModal && (
<div className="confirm-modal-overlay" onClick={() => !saving && setShowEditModal(false)}>
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
<h3 className="edit-modal-title">Edit Profile</h3>
<div className="edit-modal-avatar" onClick={() => fileInputRef.current?.click()}>
{editPhotoPreview ? (
<img src={editPhotoPreview} alt="Preview" className="edit-modal-avatar-img" />
) : (
<div className="edit-modal-avatar-placeholder">
{editName.charAt(0).toUpperCase() || 'U'}
</div>
)}
<div className="edit-modal-avatar-overlay">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</svg>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handlePhotoSelect}
/>
</div>
{editPhotoPreview && (
<button
type="button"
className="edit-modal-remove-photo"
onClick={() => setEditPhotoPreview(null)}
>
Remove photo
</button>
)}
<label className="confirm-modal-label" style={{ marginTop: '1rem' }}>Display Name</label>
<input
type="text"
className="confirm-modal-input"
value={editName}
onChange={(e) => setEditName(e.target.value)}
disabled={saving}
maxLength={50}
autoFocus
style={{ borderColor: '#d1d5db' }}
onFocus={(e) => (e.target.style.borderColor = '#22c55e')}
onBlur={(e) => (e.target.style.borderColor = '#d1d5db')}
/>
<div className="confirm-modal-actions" style={{ marginTop: '1rem' }}>
<button
type="button"
className="confirm-modal-cancel"
onClick={() => setShowEditModal(false)}
disabled={saving}
>
Cancel
</button>
<button
type="button"
className="edit-modal-save"
onClick={handleSaveProfile}
disabled={saving || !editName.trim()}
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
</div>
)}
<BottomNav />
</div>
)