save animation
This commit is contained in:
@@ -5,14 +5,37 @@ import { createEntry } from '../lib/api'
|
||||
import { encryptEntry } from '../lib/crypto'
|
||||
import BottomNav from '../components/BottomNav'
|
||||
import WelcomeModal from '../components/WelcomeModal'
|
||||
import { SaveBookAnimation } from '../components/SaveBookAnimation'
|
||||
import { useOnboardingTour, hasSeenOnboarding, markOnboardingDone } from '../hooks/useOnboardingTour'
|
||||
|
||||
const AFFIRMATIONS = [
|
||||
'You showed up for yourself today 🌱',
|
||||
'Another moment beautifully captured ✨',
|
||||
'Gratitude logged. Keep growing 🌿',
|
||||
'Small moments, big growth 🍃',
|
||||
"You're building something beautiful 💚",
|
||||
'One more grateful moment preserved 🌸',
|
||||
'Your thoughts are safe and stored 🔒',
|
||||
]
|
||||
|
||||
const SAVE_LEAVES = [
|
||||
{ left: -55, dx: -20, rot: -25, delay: 0.0, emoji: '🌱' },
|
||||
{ left: -30, dx: -8, rot: 15, delay: 0.08, emoji: '🌿' },
|
||||
{ left: -10, dx: -15, rot: -10, delay: 0.04, emoji: '🌱' },
|
||||
{ left: 5, dx: 12, rot: 20, delay: 0.12, emoji: '🍃' },
|
||||
{ left: 25, dx: 18, rot: -18, delay: 0.06, emoji: '🌿' },
|
||||
{ left: 45, dx: 25, rot: 12, delay: 0.18, emoji: '🌱' },
|
||||
{ left: -42, dx: -22, rot: 28, delay: 0.22, emoji: '🍃' },
|
||||
{ left: 18, dx: 10, rot: -22, delay: 0.28, emoji: '🌿' },
|
||||
]
|
||||
|
||||
export default function HomePage() {
|
||||
const { user, userId, secretKey, loading } = useAuth()
|
||||
const [entry, setEntry] = useState('')
|
||||
const [title, setTitle] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||
const [phase, setPhase] = useState<'idle' | 'saving' | 'book' | 'celebrate'>('idle')
|
||||
const [affirmation, setAffirmation] = useState('')
|
||||
const [message, setMessage] = useState<{ type: 'error'; text: string } | null>(null)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
|
||||
const titleInputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -61,6 +84,11 @@ export default function HomePage() {
|
||||
.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })
|
||||
.toUpperCase()
|
||||
|
||||
const handleBookDone = () => {
|
||||
setPhase('celebrate')
|
||||
setTimeout(() => setPhase('idle'), 2500)
|
||||
}
|
||||
|
||||
const handleTitleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && title.trim()) {
|
||||
e.preventDefault()
|
||||
@@ -79,7 +107,7 @@ export default function HomePage() {
|
||||
return
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
setPhase('saving')
|
||||
setMessage(null)
|
||||
|
||||
try {
|
||||
@@ -112,17 +140,14 @@ export default function HomePage() {
|
||||
token
|
||||
)
|
||||
|
||||
setMessage({ type: 'success', text: 'Entry saved securely!' })
|
||||
setTitle('')
|
||||
setEntry('')
|
||||
|
||||
// Clear success message after 3 seconds
|
||||
setTimeout(() => setMessage(null), 3000)
|
||||
setAffirmation(AFFIRMATIONS[Math.floor(Math.random() * AFFIRMATIONS.length)])
|
||||
setPhase('book')
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to save entry'
|
||||
setMessage({ type: 'error', text: errorMessage })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
setPhase('idle')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +173,7 @@ export default function HomePage() {
|
||||
onKeyDown={handleTitleKeyDown}
|
||||
enterKeyHint="next"
|
||||
ref={titleInputRef}
|
||||
disabled={saving}
|
||||
disabled={phase !== 'idle'}
|
||||
/>
|
||||
<textarea
|
||||
id="tour-content-textarea"
|
||||
@@ -158,7 +183,7 @@ export default function HomePage() {
|
||||
onChange={(e) => setEntry(e.target.value)}
|
||||
enterKeyHint="enter"
|
||||
ref={contentTextareaRef}
|
||||
disabled={saving}
|
||||
disabled={phase !== 'idle'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -168,27 +193,55 @@ export default function HomePage() {
|
||||
marginTop: '1rem',
|
||||
borderRadius: '8px',
|
||||
fontSize: '0.875rem',
|
||||
backgroundColor: message.type === 'success' ? '#f0fdf4' : '#fef2f2',
|
||||
color: message.type === 'success' ? '#15803d' : '#b91c1c',
|
||||
backgroundColor: '#fef2f2',
|
||||
color: '#b91c1c',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '0.75rem' }}>
|
||||
<button
|
||||
id="tour-save-btn"
|
||||
className="journal-write-btn"
|
||||
onClick={handleWrite}
|
||||
disabled={saving || !title.trim() || !entry.trim()}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Entry'}
|
||||
</button>
|
||||
<div style={{ marginTop: '1.5rem', position: 'relative' }}>
|
||||
{phase === 'celebrate' && (
|
||||
<>
|
||||
<div className="save-leaves" aria-hidden>
|
||||
{SAVE_LEAVES.map((leaf, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="save-leaf"
|
||||
style={{
|
||||
left: `calc(50% + ${leaf.left}px)`,
|
||||
animationDelay: `${leaf.delay}s`,
|
||||
'--leaf-dx': `${leaf.dx}px`,
|
||||
'--leaf-rot': `${leaf.rot}deg`,
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{leaf.emoji}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="save-inline-quote" role="status" aria-live="polite">
|
||||
{affirmation}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{phase !== 'celebrate' && (
|
||||
<button
|
||||
id="tour-save-btn"
|
||||
className="journal-write-btn"
|
||||
onClick={handleWrite}
|
||||
disabled={phase !== 'idle' || !title.trim() || !entry.trim()}
|
||||
>
|
||||
{phase === 'saving' ? 'Saving...' : 'Save Entry'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{phase === 'book' && <SaveBookAnimation onDone={handleBookDone} />}
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user