photo ui update
This commit is contained in:
59
src/App.css
59
src/App.css
@@ -1308,13 +1308,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-profile-name {
|
.settings-profile-name {
|
||||||
margin: 0 0 0.3rem;
|
margin: 0 0 0.15rem;
|
||||||
font-size: 1.0625rem;
|
font-size: 1.0625rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
font-family: "Sniglet", system-ui;
|
font-family: "Sniglet", system-ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-profile-email {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-family: "Sniglet", system-ui;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-profile-badge {
|
.settings-profile-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.15rem 0.5rem;
|
padding: 0.15rem 0.5rem;
|
||||||
@@ -1360,8 +1367,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
margin: 0 auto 0.5rem;
|
margin: 0 auto 0.75rem;
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -1387,30 +1393,44 @@
|
|||||||
font-family: "Sniglet", system-ui;
|
font-family: "Sniglet", system-ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-modal-avatar-overlay {
|
.edit-modal-photo-actions {
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.35);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #fff;
|
gap: 0.5rem;
|
||||||
border-radius: 50%;
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-change-photo,
|
||||||
|
.edit-modal-remove-photo {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 130px;
|
||||||
|
padding: 0.45rem 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "Sniglet", system-ui;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-change-photo {
|
||||||
|
background: var(--color-accent-light, #dcfce7);
|
||||||
|
border: 1.5px solid var(--color-primary, #22c55e);
|
||||||
|
color: var(--color-primary, #22c55e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-change-photo:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-modal-remove-photo {
|
.edit-modal-remove-photo {
|
||||||
display: block;
|
background: #fef2f2;
|
||||||
margin: 0 auto 0.5rem;
|
border: 1.5px solid #fca5a5;
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
font-size: 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-modal-remove-photo:hover {
|
.edit-modal-remove-photo:hover {
|
||||||
text-decoration: underline;
|
background: #fee2e2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-modal-save {
|
.edit-modal-save {
|
||||||
@@ -1653,7 +1673,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
padding: 0.875rem 1.125rem;
|
padding: 0.875rem 1.125rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ 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 MongoUser
|
const existingUser = await getUserByEmail(email, token) as MongoUser
|
||||||
setUserId(existingUser.id)
|
setUserId(existingUser.id)
|
||||||
setMongoUser(existingUser)
|
setMongoUser(existingUser)
|
||||||
@@ -179,7 +179,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
},
|
},
|
||||||
token
|
token
|
||||||
) as MongoUser
|
) 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)
|
setMongoUser(newUser)
|
||||||
syncReminderFromDb(newUser)
|
syncReminderFromDb(newUser)
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ async function getFcmToken(): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const swReg = await navigator.serviceWorker.ready
|
const swReg = await navigator.serviceWorker.ready
|
||||||
console.log('[FCM] Service worker ready:', swReg.active?.scriptURL)
|
// console.log('[FCM] Service worker ready:', swReg.active?.scriptURL)
|
||||||
|
|
||||||
const token = await getToken(messaging, { vapidKey: VAPID_KEY, serviceWorkerRegistration: swReg })
|
const token = await getToken(messaging, { vapidKey: VAPID_KEY, serviceWorkerRegistration: swReg })
|
||||||
if (token) {
|
if (token) {
|
||||||
console.log('[FCM] Token obtained:', token.slice(0, 20) + '…')
|
// console.log('[FCM] Token obtained:', token.slice(0, 20) + '…')
|
||||||
} else {
|
} else {
|
||||||
console.warn('[FCM] getToken returned empty — VAPID key wrong or SW not registered?')
|
console.warn('[FCM] getToken returned empty — VAPID key wrong or SW not registered?')
|
||||||
}
|
}
|
||||||
@@ -74,13 +74,13 @@ export async function enableReminder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
console.log('[FCM] Saving token and reminder settings:', { timeStr, timezone })
|
// console.log('[FCM] Saving token and reminder settings:', { timeStr, timezone })
|
||||||
|
|
||||||
await saveFcmToken(userId, fcmToken, authToken)
|
await saveFcmToken(userId, fcmToken, authToken)
|
||||||
console.log('[FCM] Token saved to backend')
|
// console.log('[FCM] Token saved to backend')
|
||||||
|
|
||||||
await saveReminderSettings(userId, { time: timeStr, enabled: true, timezone }, authToken)
|
await saveReminderSettings(userId, { time: timeStr, enabled: true, timezone }, authToken)
|
||||||
console.log('[FCM] Reminder settings saved to backend')
|
// console.log('[FCM] Reminder settings saved to backend')
|
||||||
|
|
||||||
localStorage.setItem(REMINDER_TIME_KEY, timeStr)
|
localStorage.setItem(REMINDER_TIME_KEY, timeStr)
|
||||||
localStorage.setItem(REMINDER_ENABLED_KEY, 'true')
|
localStorage.setItem(REMINDER_ENABLED_KEY, 'true')
|
||||||
@@ -113,10 +113,10 @@ export async function listenForegroundMessages(): Promise<() => void> {
|
|||||||
const messaging = await messagingPromise
|
const messaging = await messagingPromise
|
||||||
if (!messaging) return () => {}
|
if (!messaging) return () => {}
|
||||||
|
|
||||||
console.log('[FCM] Foreground message listener registered')
|
// console.log('[FCM] Foreground message listener registered')
|
||||||
|
|
||||||
const unsubscribe = onMessage(messaging, (payload) => {
|
const unsubscribe = onMessage(messaging, (payload) => {
|
||||||
console.log('[FCM] Foreground message received:', payload)
|
// console.log('[FCM] Foreground message received:', payload)
|
||||||
const title = payload.notification?.title || 'Grateful Journal 🌱'
|
const title = payload.notification?.title || 'Grateful Journal 🌱'
|
||||||
const body = payload.notification?.body || "You haven't written today yet."
|
const body = payload.notification?.body || "You haven't written today yet."
|
||||||
if (Notification.permission !== 'granted') {
|
if (Notification.permission !== 'granted') {
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ export default function SettingsPage() {
|
|||||||
</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>
|
||||||
|
{user?.email && <p className="settings-profile-email">{user.email}</p>}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="settings-edit-btn" onClick={openEditModal} title="Edit profile">
|
<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">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
@@ -358,11 +359,11 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Privacy & Security */}
|
{/* Privacy & Security */}
|
||||||
<section className="settings-section">
|
{/* <section className="settings-section"> */}
|
||||||
<h3 className="settings-section-title">PRIVACY & SECURITY</h3>
|
{/* <h3 className="settings-section-title">PRIVACY & SECURITY</h3> */}
|
||||||
<div className="settings-card">
|
{/* <div className="settings-card"> */}
|
||||||
{/* Passcode Lock — disabled for now, toggle is non-functional */}
|
{/* Passcode Lock — disabled for now, toggle is non-functional */}
|
||||||
<div className="settings-item" style={{ opacity: 0.5 }}>
|
{/* <div className="settings-item" style={{ opacity: 0.5 }}>
|
||||||
<div className="settings-item-icon settings-item-icon-green">
|
<div className="settings-item-icon settings-item-icon-green">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||||
@@ -382,7 +383,7 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
<span className="settings-toggle-slider" style={{ cursor: 'not-allowed' }}></span>
|
<span className="settings-toggle-slider" style={{ cursor: 'not-allowed' }}></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Face ID — commented out for future use */}
|
{/* Face ID — commented out for future use */}
|
||||||
{/*
|
{/*
|
||||||
@@ -413,8 +414,8 @@ export default function SettingsPage() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
*/}
|
*/}
|
||||||
</div>
|
{/* </div> */}
|
||||||
</section>
|
{/* </section> */}
|
||||||
|
|
||||||
{/* App */}
|
{/* App */}
|
||||||
<section className="settings-section">
|
<section className="settings-section">
|
||||||
@@ -477,7 +478,7 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{/* Data & Look */}
|
{/* Data & Look */}
|
||||||
<section className="settings-section">
|
<section className="settings-section">
|
||||||
<h3 className="settings-section-title">DATA & LOOK</h3>
|
<h3 className="settings-section-title">CUSTOMIZATION</h3>
|
||||||
<div className="settings-card">
|
<div className="settings-card">
|
||||||
{/* Export Journal — commented out for future use */}
|
{/* Export Journal — commented out for future use */}
|
||||||
{/*
|
{/*
|
||||||
@@ -580,11 +581,11 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{/* Clear Data */}
|
{/* Clear Data */}
|
||||||
<button type="button" className="settings-clear-btn" onClick={handleClearData}>
|
<button type="button" className="settings-clear-btn" onClick={handleClearData}>
|
||||||
<span>Clear All Data</span>
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<polyline points="3 6 5 6 21 6"></polyline>
|
<polyline points="3 6 5 6 21 6"></polyline>
|
||||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
<span>Clear All Data</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Sign Out */}
|
{/* Sign Out */}
|
||||||
@@ -650,14 +651,14 @@ export default function SettingsPage() {
|
|||||||
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
<h3 className="edit-modal-title">Edit Profile</h3>
|
<h3 className="edit-modal-title">Edit Profile</h3>
|
||||||
|
|
||||||
<label className="edit-modal-avatar" style={{ cursor: 'pointer' }}>
|
<input
|
||||||
<input
|
ref={fileInputRef}
|
||||||
ref={fileInputRef}
|
type="file"
|
||||||
type="file"
|
accept="image/*"
|
||||||
accept="image/*"
|
style={{ display: 'none' }}
|
||||||
style={{ display: 'none' }}
|
onChange={handlePhotoSelect}
|
||||||
onChange={handlePhotoSelect}
|
/>
|
||||||
/>
|
<div className="edit-modal-avatar">
|
||||||
{editPhotoPreview ? (
|
{editPhotoPreview ? (
|
||||||
<img src={editPhotoPreview} alt="Preview" className="edit-modal-avatar-img"
|
<img src={editPhotoPreview} alt="Preview" className="edit-modal-avatar-img"
|
||||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
||||||
@@ -667,22 +668,25 @@ export default function SettingsPage() {
|
|||||||
{editName.charAt(0).toUpperCase() || 'U'}
|
{editName.charAt(0).toUpperCase() || 'U'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="edit-modal-avatar-overlay">
|
</div>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<div className="edit-modal-photo-actions">
|
||||||
<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>
|
|
||||||
</label>
|
|
||||||
{editPhotoPreview && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="edit-modal-remove-photo"
|
className="edit-modal-change-photo"
|
||||||
onClick={() => setEditPhotoPreview(null)}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
Remove photo
|
Change photo
|
||||||
</button>
|
</button>
|
||||||
)}
|
{editPhotoPreview && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="edit-modal-remove-photo"
|
||||||
|
onClick={() => setEditPhotoPreview(null)}
|
||||||
|
>
|
||||||
|
Remove photo
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<label className="confirm-modal-label" style={{ marginTop: '1rem' }}>Display Name</label>
|
<label className="confirm-modal-label" style={{ marginTop: '1rem' }}>Display Name</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user