Compare commits
2 Commits
fa10677e41
...
feb6c10417
| Author | SHA1 | Date | |
|---|---|---|---|
| feb6c10417 | |||
| 2b293a20b7 |
97
src/App.css
97
src/App.css
@@ -12,46 +12,29 @@
|
||||
}
|
||||
|
||||
/* ============================
|
||||
PROTECTED ROUTE SPINNER
|
||||
PAGE LOADER (swaying tree)
|
||||
============================ */
|
||||
.protected-route__loading {
|
||||
.page-loader {
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
background: #eef6ee;
|
||||
color: #9ca3af;
|
||||
background: var(--color-bg-soft, #eef6ee);
|
||||
}
|
||||
|
||||
.protected-route__spinner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 3px solid #e5e7eb;
|
||||
border-top-color: #22c55e;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
.page-loader__tree {
|
||||
transform-origin: center bottom;
|
||||
animation: tree-sway 1.8s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
@keyframes tree-sway {
|
||||
from { transform: rotate(-5deg); }
|
||||
to { transform: rotate(5deg); }
|
||||
}
|
||||
|
||||
/* ============================
|
||||
LOGIN PAGE
|
||||
============================ */
|
||||
/* ── Loading state ──────────────────────────────────────── */
|
||||
.login-page__spinner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 3px solid #e5e7eb;
|
||||
border-top-color: #22c55e;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
/* ── Login page — two-panel layout ─────────────────────── */
|
||||
.lp {
|
||||
@@ -62,18 +45,6 @@
|
||||
background: linear-gradient(160deg, #eef6ee 0%, #dcfce7 100%);
|
||||
}
|
||||
|
||||
/* Loading state wrapper */
|
||||
.lp--loading {
|
||||
grid-template-columns: 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
background: var(--color-bg-soft);
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* ── Left: animated tree hero ───────────────────────────── */
|
||||
.lp__hero {
|
||||
display: flex;
|
||||
@@ -835,6 +806,17 @@
|
||||
[data-theme="dark"] .sba-overlay {
|
||||
background: rgba(10, 18, 10, 0.76);
|
||||
}
|
||||
[data-theme="dark"] .sba-left-group rect {
|
||||
fill: #1a2e1a;
|
||||
stroke: #2d4a2d;
|
||||
}
|
||||
[data-theme="dark"] .sba-left-group line {
|
||||
stroke: #2d4a2d;
|
||||
}
|
||||
[data-theme="dark"] .sba-right-group rect {
|
||||
fill: #162416;
|
||||
stroke: #2d4a2d;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
BOTTOM NAVIGATION — Static flex item, always at bottom
|
||||
@@ -1252,7 +1234,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #f9a8d4 0%, #f472b6 100%);
|
||||
background: linear-gradient(135deg, #86efac 0%, #22c55e 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -2199,6 +2181,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================
|
||||
ALERT MESSAGES
|
||||
============================ */
|
||||
.alert-msg {
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
}
|
||||
.alert-msg--error {
|
||||
background-color: #fef2f2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
.alert-msg--success {
|
||||
background-color: #f0fdf4;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
DARK THEME
|
||||
============================ */
|
||||
@@ -2504,16 +2504,19 @@
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
/* -- Spinners -- */
|
||||
[data-theme="dark"] .protected-route__spinner,
|
||||
[data-theme="dark"] .login-page__spinner {
|
||||
border-color: #2a2a2a;
|
||||
border-top-color: #4ade80;
|
||||
/* -- Page loader -- */
|
||||
[data-theme="dark"] .page-loader {
|
||||
background: #0f0f0f;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .protected-route__loading {
|
||||
background: #0f0f0f;
|
||||
color: #5a6a5a;
|
||||
/* -- Alert messages -- */
|
||||
[data-theme="dark"] .alert-msg--error {
|
||||
background-color: rgba(185, 28, 28, 0.18);
|
||||
color: #fca5a5;
|
||||
}
|
||||
[data-theme="dark"] .alert-msg--success {
|
||||
background-color: rgba(21, 128, 61, 0.18);
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
/* -- Icon btn hover -- */
|
||||
|
||||
26
src/components/PageLoader.tsx
Normal file
26
src/components/PageLoader.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
export function PageLoader() {
|
||||
return (
|
||||
<div className="page-loader" role="status" aria-label="Loading">
|
||||
<svg
|
||||
className="page-loader__tree"
|
||||
viewBox="0 0 60 90"
|
||||
width="72"
|
||||
height="72"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Trunk */}
|
||||
<rect x="26" y="58" width="8" height="28" rx="4" fill="#A0722A" />
|
||||
{/* Side canopy depth */}
|
||||
<circle cx="14" cy="52" r="14" fill="#16a34a" />
|
||||
<circle cx="46" cy="52" r="14" fill="#16a34a" />
|
||||
{/* Main canopy */}
|
||||
<circle cx="30" cy="37" r="22" fill="#22c55e" />
|
||||
{/* Light highlight */}
|
||||
<circle cx="20" cy="27" r="10" fill="#4ade80" opacity="0.6" />
|
||||
{/* Top tip */}
|
||||
<circle cx="30" cy="17" r="10" fill="#4ade80" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type ReactNode } from 'react'
|
||||
import { Navigate, useLocation } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { PageLoader } from './PageLoader'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
@@ -11,12 +12,7 @@ export function ProtectedRoute({ children }: Props) {
|
||||
const location = useLocation()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="protected-route__loading" aria-live="polite">
|
||||
<span className="protected-route__spinner" aria-hidden />
|
||||
<p>Loading…</p>
|
||||
</div>
|
||||
)
|
||||
return <PageLoader />
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -130,7 +130,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
try {
|
||||
console.log('[Auth] Fetching user by email:', email)
|
||||
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)
|
||||
setMongoUser(existingUser)
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { decryptEntry } from '../lib/crypto'
|
||||
import { formatIST, getISTDateComponents } from '../lib/timezone'
|
||||
import BottomNav from '../components/BottomNav'
|
||||
import { useOnboardingTour, hasPendingTourStep, clearPendingTourStep } from '../hooks/useOnboardingTour'
|
||||
import { PageLoader } from '../components/PageLoader'
|
||||
|
||||
interface DecryptedEntry extends JournalEntry {
|
||||
decryptedTitle?: string
|
||||
@@ -194,12 +195,7 @@ export default function HistoryPage() {
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="history-page" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<p style={{ color: '#9ca3af' }}>Loading…</p>
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
return <PageLoader />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -274,13 +270,13 @@ export default function HistoryPage() {
|
||||
</h3>
|
||||
|
||||
{loadingEntries ? (
|
||||
<p style={{ color: '#9ca3af', fontSize: '0.875rem', textAlign: 'center', padding: '1.5rem 0', fontFamily: '"Sniglet", system-ui' }}>
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem', textAlign: 'center', padding: '1.5rem 0', fontFamily: '"Sniglet", system-ui' }}>
|
||||
Loading entries…
|
||||
</p>
|
||||
) : (
|
||||
<div className="entries-list">
|
||||
{selectedDateEntries.length === 0 ? (
|
||||
<p style={{ color: '#9ca3af', fontSize: '0.875rem', textAlign: 'center', padding: '1.5rem 0', fontFamily: '"Sniglet", system-ui' }}>
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem', textAlign: 'center', padding: '1.5rem 0', fontFamily: '"Sniglet", system-ui' }}>
|
||||
No entries for this day yet. Start writing!
|
||||
</p>
|
||||
) : (
|
||||
|
||||
@@ -7,6 +7,7 @@ import BottomNav from '../components/BottomNav'
|
||||
import WelcomeModal from '../components/WelcomeModal'
|
||||
import { SaveBookAnimation } from '../components/SaveBookAnimation'
|
||||
import { useOnboardingTour, hasSeenOnboarding, markOnboardingDone } from '../hooks/useOnboardingTour'
|
||||
import { PageLoader } from '../components/PageLoader'
|
||||
|
||||
const AFFIRMATIONS = [
|
||||
'You showed up for yourself today 🌱',
|
||||
@@ -61,18 +62,14 @@ export default function HomePage() {
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="home-page" style={{ alignItems: 'center', justifyContent: 'center' }}>
|
||||
<p style={{ color: '#9ca3af' }}>Loading…</p>
|
||||
</div>
|
||||
)
|
||||
return <PageLoader />
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="home-page" style={{ alignItems: 'center', justifyContent: 'center', gap: '1rem' }}>
|
||||
<h1 style={{ fontFamily: '"Sniglet", system-ui', color: '#1a1a1a' }}>Grateful Journal</h1>
|
||||
<p style={{ color: '#6b7280' }}>Sign in to start your journal.</p>
|
||||
<h1 style={{ fontFamily: '"Sniglet", system-ui', color: 'var(--color-text)' }}>Grateful Journal</h1>
|
||||
<p style={{ color: 'var(--color-text-muted)' }}>Sign in to start your journal.</p>
|
||||
<Link to="/login" className="home-login-link">Go to login</Link>
|
||||
</div>
|
||||
)
|
||||
@@ -188,15 +185,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div style={{
|
||||
padding: '0.75rem',
|
||||
marginTop: '1rem',
|
||||
borderRadius: '8px',
|
||||
fontSize: '0.875rem',
|
||||
backgroundColor: '#fef2f2',
|
||||
color: '#b91c1c',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<div className="alert-msg alert-msg--error" style={{ marginTop: '1rem' }}>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { GoogleSignInButton } from '../components/GoogleSignInButton'
|
||||
import { TreeAnimation } from '../components/TreeAnimation'
|
||||
import { PageLoader } from '../components/PageLoader'
|
||||
|
||||
export default function LoginPage() {
|
||||
const { user, loading, signInWithGoogle, authError } = useAuth()
|
||||
@@ -28,12 +29,7 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="lp lp--loading" aria-live="polite">
|
||||
<span className="login-page__spinner" aria-hidden />
|
||||
<p>Loading…</p>
|
||||
</div>
|
||||
)
|
||||
return <PageLoader />
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { clearDeviceKey, clearEncryptedSecretKey } from '../lib/crypto'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import BottomNav from '../components/BottomNav'
|
||||
import { useOnboardingTour, hasPendingTourStep, clearPendingTourStep } from '../hooks/useOnboardingTour'
|
||||
import { PageLoader } from '../components/PageLoader'
|
||||
|
||||
const MAX_PHOTO_SIZE = 200 // px — resize to 200x200
|
||||
|
||||
@@ -187,12 +188,7 @@ export default function SettingsPage() {
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="settings-page" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<p style={{ color: '#9ca3af' }}>Loading…</p>
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
return <PageLoader />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -211,7 +207,7 @@ export default function SettingsPage() {
|
||||
{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%)' }}>
|
||||
<div className="settings-avatar-placeholder" style={{ fontSize: '1.75rem' }}>
|
||||
{displayName.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
@@ -345,15 +341,7 @@ export default function SettingsPage() {
|
||||
</section>
|
||||
|
||||
{message && (
|
||||
<div style={{
|
||||
padding: '0.75rem',
|
||||
marginBottom: '1rem',
|
||||
borderRadius: '8px',
|
||||
fontSize: '0.875rem',
|
||||
backgroundColor: message.type === 'success' ? '#f0fdf4' : '#fef2f2',
|
||||
color: message.type === 'success' ? '#15803d' : '#b91c1c',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<div className={`alert-msg alert-msg--${message.type}`} style={{ marginBottom: '1rem' }}>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
@@ -481,9 +469,8 @@ export default function SettingsPage() {
|
||||
disabled={saving}
|
||||
maxLength={50}
|
||||
autoFocus
|
||||
style={{ borderColor: '#d1d5db' }}
|
||||
onFocus={(e) => (e.target.style.borderColor = '#22c55e')}
|
||||
onBlur={(e) => (e.target.style.borderColor = '#d1d5db')}
|
||||
onFocus={(e) => (e.target.style.borderColor = 'var(--color-primary)')}
|
||||
onBlur={(e) => (e.target.style.borderColor = '')}
|
||||
/>
|
||||
|
||||
<div className="confirm-modal-actions" style={{ marginTop: '1rem' }}>
|
||||
|
||||
Reference in New Issue
Block a user