added image upload feature
This commit is contained in:
121
src/App.css
121
src/App.css
@@ -754,6 +754,114 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-edit-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: none;
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #22c55e;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-edit-btn:hover {
|
||||||
|
background: #dcfce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-family: "Sniglet", system-ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-avatar {
|
||||||
|
position: relative;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin: 0 auto 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-avatar-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-avatar-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #86efac 0%, #22c55e 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
font-family: "Sniglet", system-ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-avatar-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-remove-photo {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 0.5rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-remove-photo:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-save {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.65rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #22c55e;
|
||||||
|
color: #fff;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-save:hover:not(:disabled) {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-save:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-section {
|
.settings-section {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
@@ -1715,6 +1823,19 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .settings-edit-btn {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .settings-edit-btn:hover {
|
||||||
|
background: rgba(34, 197, 94, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .edit-modal-title {
|
||||||
|
color: #e8f5e8;
|
||||||
|
}
|
||||||
|
|
||||||
/* -- Settings divider -- */
|
/* -- Settings divider -- */
|
||||||
[data-theme="dark"] .settings-divider {
|
[data-theme="dark"] .settings-divider {
|
||||||
background: #2a2a2a;
|
background: #2a2a2a;
|
||||||
|
|||||||
@@ -29,13 +29,23 @@ import {
|
|||||||
getEncryptedSecretKey,
|
getEncryptedSecretKey,
|
||||||
} from '../lib/crypto'
|
} from '../lib/crypto'
|
||||||
|
|
||||||
|
type MongoUser = {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
displayName?: string
|
||||||
|
photoURL?: string
|
||||||
|
theme?: string
|
||||||
|
}
|
||||||
|
|
||||||
type AuthContextValue = {
|
type AuthContextValue = {
|
||||||
user: User | null
|
user: User | null
|
||||||
userId: string | null
|
userId: string | null
|
||||||
|
mongoUser: MongoUser | null
|
||||||
loading: boolean
|
loading: boolean
|
||||||
secretKey: Uint8Array | null
|
secretKey: Uint8Array | null
|
||||||
signInWithGoogle: () => Promise<void>
|
signInWithGoogle: () => Promise<void>
|
||||||
signOut: () => Promise<void>
|
signOut: () => Promise<void>
|
||||||
|
refreshMongoUser: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextValue | null>(null)
|
const AuthContext = createContext<AuthContextValue | null>(null)
|
||||||
@@ -43,6 +53,7 @@ const AuthContext = createContext<AuthContextValue | null>(null)
|
|||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [user, setUser] = useState<User | null>(null)
|
const [user, setUser] = useState<User | null>(null)
|
||||||
const [userId, setUserId] = useState<string | null>(null)
|
const [userId, setUserId] = useState<string | null>(null)
|
||||||
|
const [mongoUser, setMongoUser] = useState<MongoUser | null>(null)
|
||||||
const [secretKey, setSecretKey] = useState<Uint8Array | null>(null)
|
const [secretKey, setSecretKey] = useState<Uint8Array | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
@@ -114,9 +125,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// Try to get existing user
|
// Try to get existing user
|
||||||
try {
|
try {
|
||||||
console.log('[Auth] Fetching user by email:', email)
|
console.log('[Auth] Fetching user by email:', email)
|
||||||
const existingUser = await getUserByEmail(email, token) as { id: string }
|
const existingUser = await getUserByEmail(email, token) as MongoUser
|
||||||
console.log('[Auth] Found existing user:', existingUser.id)
|
console.log('[Auth] Found existing user:', existingUser.id)
|
||||||
setUserId(existingUser.id)
|
setUserId(existingUser.id)
|
||||||
|
setMongoUser(existingUser)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Auth] User not found, registering...', error)
|
console.warn('[Auth] User not found, registering...', error)
|
||||||
// User doesn't exist, register them
|
// User doesn't exist, register them
|
||||||
@@ -127,9 +139,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
photoURL: authUser.photoURL || undefined,
|
photoURL: authUser.photoURL || undefined,
|
||||||
},
|
},
|
||||||
token
|
token
|
||||||
) as { id: string }
|
) as MongoUser
|
||||||
console.log('[Auth] Registered new user:', newUser.id)
|
console.log('[Auth] Registered new user:', newUser.id)
|
||||||
setUserId(newUser.id)
|
setUserId(newUser.id)
|
||||||
|
setMongoUser(newUser)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Auth] Error syncing user with database:', error)
|
console.error('[Auth] Error syncing user with database:', error)
|
||||||
@@ -148,6 +161,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setUserId(null)
|
setUserId(null)
|
||||||
|
setMongoUser(null)
|
||||||
setSecretKey(null)
|
setSecretKey(null)
|
||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -160,9 +174,22 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
await signInWithPopup(auth, googleProvider)
|
await signInWithPopup(auth, googleProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshMongoUser() {
|
||||||
|
if (!user) return
|
||||||
|
try {
|
||||||
|
const token = await user.getIdToken()
|
||||||
|
const email = user.email!
|
||||||
|
const updated = await getUserByEmail(email, token) as MongoUser
|
||||||
|
setMongoUser(updated)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Auth] Error refreshing mongo user:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function signOut() {
|
async function signOut() {
|
||||||
// Clear secret key from memory
|
// Clear secret key from memory
|
||||||
setSecretKey(null)
|
setSecretKey(null)
|
||||||
|
setMongoUser(null)
|
||||||
// Reset onboarding so tour shows again on next login
|
// Reset onboarding so tour shows again on next login
|
||||||
localStorage.removeItem('gj-onboarding-done')
|
localStorage.removeItem('gj-onboarding-done')
|
||||||
localStorage.removeItem('gj-tour-pending-step')
|
localStorage.removeItem('gj-tour-pending-step')
|
||||||
@@ -175,10 +202,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const value: AuthContextValue = {
|
const value: AuthContextValue = {
|
||||||
user,
|
user,
|
||||||
userId,
|
userId,
|
||||||
|
mongoUser,
|
||||||
secretKey,
|
secretKey,
|
||||||
loading,
|
loading,
|
||||||
signInWithGoogle,
|
signInWithGoogle,
|
||||||
signOut,
|
signOut,
|
||||||
|
refreshMongoUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export async function updateUserProfile(
|
|||||||
updates: { displayName?: string; photoURL?: string; theme?: string },
|
updates: { displayName?: string; photoURL?: string; theme?: string },
|
||||||
token: string
|
token: string
|
||||||
) {
|
) {
|
||||||
return apiCall(`/api/users/update/${userId}`, {
|
return apiCall(`/api/users/${userId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: updates,
|
body: updates,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -1,13 +1,40 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
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 { clearDeviceKey, clearEncryptedSecretKey } from '../lib/crypto'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import BottomNav from '../components/BottomNav'
|
import BottomNav from '../components/BottomNav'
|
||||||
import { useOnboardingTour, hasPendingTourStep, clearPendingTourStep } from '../hooks/useOnboardingTour'
|
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() {
|
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 [passcodeEnabled, setPasscodeEnabled] = useState(false) // Passcode lock — disabled for now
|
||||||
// const [faceIdEnabled, setFaceIdEnabled] = useState(false) // Face ID — disabled for now
|
// const [faceIdEnabled, setFaceIdEnabled] = useState(false) // Face ID — disabled for now
|
||||||
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
|
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
|
||||||
@@ -22,6 +49,13 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
const { continueTourOnSettings } = useOnboardingTour()
|
const { continueTourOnSettings } = useOnboardingTour()
|
||||||
const navigate = useNavigate()
|
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
|
// Continue onboarding tour if navigated here from the history page tour
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -37,7 +71,53 @@ export default function SettingsPage() {
|
|||||||
navigate('/')
|
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
|
// Apply theme to DOM
|
||||||
const applyTheme = useCallback((t: 'light' | 'dark') => {
|
const applyTheme = useCallback((t: 'light' | 'dark') => {
|
||||||
@@ -127,14 +207,23 @@ export default function SettingsPage() {
|
|||||||
{/* Profile Section */}
|
{/* Profile Section */}
|
||||||
<div className="settings-profile">
|
<div className="settings-profile">
|
||||||
<div className="settings-avatar">
|
<div className="settings-avatar">
|
||||||
<div className="settings-avatar-placeholder" style={{ fontSize: '1.75rem', background: 'linear-gradient(135deg, #86efac 0%, #22c55e 100%)' }}>
|
{photoURL ? (
|
||||||
🍀
|
<img src={photoURL} alt={displayName} className="settings-avatar-img" />
|
||||||
</div>
|
) : (
|
||||||
|
<div className="settings-avatar-placeholder" style={{ fontSize: '1.75rem', background: 'linear-gradient(135deg, #86efac 0%, #22c55e 100%)' }}>
|
||||||
|
{displayName.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-profile-info">
|
<div className="settings-profile-info">
|
||||||
<h2 className="settings-profile-name">{displayName}</h2>
|
<h2 className="settings-profile-name">{displayName}</h2>
|
||||||
{/* <span className="settings-profile-badge">PRO MEMBER</span> */}
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Privacy & Security */}
|
{/* Privacy & Security */}
|
||||||
@@ -339,6 +428,80 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</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 />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user