save animation
This commit is contained in:
@@ -12,7 +12,9 @@
|
|||||||
"Bash(conda run:*)",
|
"Bash(conda run:*)",
|
||||||
"Bash(git rm:*)",
|
"Bash(git rm:*)",
|
||||||
"Bash(git remote:*)",
|
"Bash(git remote:*)",
|
||||||
"Bash(find /Users/jeet/Desktop/Jio/grateful-journal/src -type f -name *.ts -o -name *.tsx)"
|
"Bash(find /Users/jeet/Desktop/Jio/grateful-journal/src -type f -name *.ts -o -name *.tsx)",
|
||||||
|
"Bash(ls -la /Users/jeet/Desktop/Jio/grateful-journal/*.config.*)",
|
||||||
|
"mcp__ide__getDiagnostics"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
231
src/App.css
231
src/App.css
@@ -575,6 +575,94 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Save success animations ──────────────────────────────── */
|
||||||
|
|
||||||
|
/* Card glows green on save */
|
||||||
|
.journal-card {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-card--saved {
|
||||||
|
animation: save-card-glow 0.7s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes save-card-glow {
|
||||||
|
0% { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.07); }
|
||||||
|
35% { box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.35), 0 6px 32px rgba(34, 197, 94, 0.22); }
|
||||||
|
100% { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.07); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button pops into a checkmark */
|
||||||
|
.journal-write-btn--saved {
|
||||||
|
pointer-events: none;
|
||||||
|
animation: save-btn-pop 0.45s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes save-btn-pop {
|
||||||
|
0% { transform: scale(0.88); }
|
||||||
|
60% { transform: scale(1.1); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leaf burst particles */
|
||||||
|
.save-leaves {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-leaf {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0;
|
||||||
|
animation: save-leaf-float 1.9s ease-out forwards;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes save-leaf-float {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0) translateX(0) rotate(0deg) scale(0.6);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
12% { opacity: 1; transform: translateY(-18px) translateX(calc(var(--leaf-dx) * 0.1)) rotate(calc(var(--leaf-rot) * 0.1)) scale(1); }
|
||||||
|
85% { opacity: 0.5; }
|
||||||
|
100% {
|
||||||
|
transform: translateY(-190px) translateX(var(--leaf-dx)) rotate(var(--leaf-rot)) scale(0.4);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Affirmation quote — centered, appears after book animation */
|
||||||
|
.save-inline-quote {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1.25rem;
|
||||||
|
min-height: 44px;
|
||||||
|
border-radius: 100px;
|
||||||
|
background: #f0fdf4;
|
||||||
|
border: 1.5px solid #bbf7d0;
|
||||||
|
font-family: "Sniglet", system-ui;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #15803d;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
animation: save-inline-quote-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes save-inline-quote-in {
|
||||||
|
0% { opacity: 0; transform: scale(0.88) translateY(4px); }
|
||||||
|
100% { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
.journal-icon-btn {
|
.journal-icon-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -594,6 +682,142 @@
|
|||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================
|
||||||
|
SAVE BOOK ANIMATION OVERLAY
|
||||||
|
============================ */
|
||||||
|
|
||||||
|
.sba-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 300;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(238, 246, 238, 0.72);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
animation:
|
||||||
|
sba-overlay-in 0.3s ease forwards,
|
||||||
|
sba-overlay-out 0.4s ease 2.5s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sba-overlay-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sba-overlay-out {
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sba-wrap {
|
||||||
|
animation: sba-wrap-in 0.45s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sba-wrap-in {
|
||||||
|
from { transform: scale(0.6); opacity: 0; }
|
||||||
|
to { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sba-svg {
|
||||||
|
width: min(280px, 74vw);
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shadow gently expands when book closes */
|
||||||
|
.sba-shadow {
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
animation: sba-shadow-grow 0.4s ease 1.95s forwards;
|
||||||
|
}
|
||||||
|
@keyframes sba-shadow-grow {
|
||||||
|
0% { transform: scaleX(1); opacity: 1; }
|
||||||
|
50% { transform: scaleX(1.18); opacity: 1; }
|
||||||
|
100% { transform: scaleX(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Writing lines draw via stroke-dashoffset */
|
||||||
|
.sba-line {
|
||||||
|
stroke-dasharray: 76;
|
||||||
|
stroke-dashoffset: 76;
|
||||||
|
}
|
||||||
|
.sba-line-1 { animation: sba-line-draw 0.28s ease forwards 0.35s; }
|
||||||
|
.sba-line-2 { animation: sba-line-draw 0.28s ease forwards 0.65s; }
|
||||||
|
.sba-line-3 { animation: sba-line-draw 0.25s ease forwards 0.95s; }
|
||||||
|
.sba-line-4 { animation: sba-line-draw 0.22s ease forwards 1.2s; }
|
||||||
|
|
||||||
|
@keyframes sba-line-draw {
|
||||||
|
to { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pen moves down through each line then flies off */
|
||||||
|
.sba-pen {
|
||||||
|
animation: sba-pen-write 1.12s ease forwards 0.3s;
|
||||||
|
}
|
||||||
|
@keyframes sba-pen-write {
|
||||||
|
0% { transform: translate(208px, 42px) rotate(20deg); opacity: 1; }
|
||||||
|
17% { transform: translate(208px, 42px) rotate(20deg); opacity: 1; }
|
||||||
|
28% { transform: translate(208px, 64px) rotate(20deg); opacity: 1; }
|
||||||
|
45% { transform: translate(208px, 64px) rotate(20deg); opacity: 1; }
|
||||||
|
57% { transform: translate(198px, 86px) rotate(20deg); opacity: 1; }
|
||||||
|
72% { transform: translate(198px, 86px) rotate(20deg); opacity: 1; }
|
||||||
|
82% { transform: translate(191px, 108px) rotate(20deg); opacity: 1; }
|
||||||
|
94% { transform: translate(191px, 108px) rotate(20deg); opacity: 1; }
|
||||||
|
100% { transform: translate(244px, 148px) rotate(20deg); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left page folds toward the spine (right edge) */
|
||||||
|
.sba-left-group {
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: 100% 50%;
|
||||||
|
animation: sba-fold 0.42s cubic-bezier(0.4, 0, 0.9, 0.6) 1.45s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right page folds toward the spine (left edge) */
|
||||||
|
.sba-right-group {
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: 0% 50%;
|
||||||
|
animation: sba-fold 0.42s cubic-bezier(0.4, 0, 0.9, 0.6) 1.57s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sba-fold {
|
||||||
|
to { transform: scaleX(0); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spine fades as pages close */
|
||||||
|
.sba-spine {
|
||||||
|
animation: sba-fade 0.3s ease 1.65s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sba-fade {
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Closed book springs in */
|
||||||
|
.sba-closed-book {
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.78);
|
||||||
|
animation: sba-closed-in 0.48s cubic-bezier(0.175, 0.885, 0.32, 1.275) 1.95s forwards;
|
||||||
|
}
|
||||||
|
@keyframes sba-closed-in {
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkmark draws itself on the cover */
|
||||||
|
.sba-check {
|
||||||
|
stroke-dasharray: 135;
|
||||||
|
stroke-dashoffset: 135;
|
||||||
|
animation: sba-line-draw 0.55s ease forwards 2.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode */
|
||||||
|
[data-theme="dark"] .sba-overlay {
|
||||||
|
background: rgba(10, 18, 10, 0.76);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================
|
/* ============================
|
||||||
BOTTOM NAVIGATION — Static flex item, always at bottom
|
BOTTOM NAVIGATION — Static flex item, always at bottom
|
||||||
============================ */
|
============================ */
|
||||||
@@ -2019,6 +2243,13 @@
|
|||||||
0 0 32px rgba(74, 222, 128, 0.15);
|
0 0 32px rgba(74, 222, 128, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -- Save inline quote dark mode -- */
|
||||||
|
[data-theme="dark"] .save-inline-quote {
|
||||||
|
background: #1e2a1e;
|
||||||
|
border-color: rgba(74, 222, 128, 0.25);
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
/* -- Home login link -- */
|
/* -- Home login link -- */
|
||||||
[data-theme="dark"] .home-login-link {
|
[data-theme="dark"] .home-login-link {
|
||||||
background: #22c55e;
|
background: #22c55e;
|
||||||
|
|||||||
72
src/components/SaveBookAnimation.tsx
Normal file
72
src/components/SaveBookAnimation.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export function SaveBookAnimation({ onDone }: { onDone: () => void }) {
|
||||||
|
useEffect(() => {
|
||||||
|
const t = setTimeout(onDone, 2900)
|
||||||
|
return () => clearTimeout(t)
|
||||||
|
}, [onDone])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sba-overlay" aria-hidden="true">
|
||||||
|
<div className="sba-wrap">
|
||||||
|
<svg viewBox="0 0 260 185" fill="none" xmlns="http://www.w3.org/2000/svg" className="sba-svg">
|
||||||
|
{/* Drop shadow */}
|
||||||
|
<ellipse className="sba-shadow" cx="130" cy="172" rx="74" ry="9" fill="rgba(34,197,94,0.14)" />
|
||||||
|
|
||||||
|
{/* LEFT PAGE */}
|
||||||
|
<g className="sba-left-group">
|
||||||
|
<rect x="22" y="18" width="98" height="140" rx="4" fill="#ffffff" stroke="#d4e8d4" strokeWidth="1.5" />
|
||||||
|
<line x1="34" y1="50" x2="108" y2="50" stroke="#edf7ed" strokeWidth="1" />
|
||||||
|
<line x1="34" y1="66" x2="108" y2="66" stroke="#edf7ed" strokeWidth="1" />
|
||||||
|
<line x1="34" y1="82" x2="108" y2="82" stroke="#edf7ed" strokeWidth="1" />
|
||||||
|
<line x1="34" y1="98" x2="108" y2="98" stroke="#edf7ed" strokeWidth="1" />
|
||||||
|
<line x1="34" y1="114" x2="108" y2="114" stroke="#edf7ed" strokeWidth="1" />
|
||||||
|
<line x1="34" y1="130" x2="108" y2="130" stroke="#edf7ed" strokeWidth="1" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* SPINE */}
|
||||||
|
<g className="sba-spine">
|
||||||
|
<rect x="119" y="16" width="7" height="144" rx="2.5" fill="#22c55e" opacity="0.45" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* RIGHT PAGE (writing lines live here — folds independently) */}
|
||||||
|
<g className="sba-right-group">
|
||||||
|
<rect x="126" y="18" width="98" height="140" rx="4" fill="#f7fdf5" stroke="#d4e8d4" strokeWidth="1.5" />
|
||||||
|
<line className="sba-line sba-line-1" x1="138" y1="50" x2="212" y2="50" stroke="#22c55e" strokeWidth="2.5" strokeLinecap="round" />
|
||||||
|
<line className="sba-line sba-line-2" x1="138" y1="72" x2="212" y2="72" stroke="#22c55e" strokeWidth="2.5" strokeLinecap="round" />
|
||||||
|
<line className="sba-line sba-line-3" x1="138" y1="94" x2="202" y2="94" stroke="#16a34a" strokeWidth="2.5" strokeLinecap="round" />
|
||||||
|
<line className="sba-line sba-line-4" x1="138" y1="116" x2="195" y2="116" stroke="#16a34a" strokeWidth="2.5" strokeLinecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* PEN — independent so it doesn't fold with the page */}
|
||||||
|
<g className="sba-pen">
|
||||||
|
{/* body */}
|
||||||
|
<rect x="-3.5" y="-24" width="7" height="22" rx="2.5" fill="#374151" />
|
||||||
|
{/* metal band */}
|
||||||
|
<rect x="-3.5" y="-5" width="7" height="3" fill="#9ca3af" />
|
||||||
|
{/* nib */}
|
||||||
|
<polygon points="-3.5,-2 3.5,-2 0,7" fill="#f59e0b" />
|
||||||
|
{/* ink dot */}
|
||||||
|
<circle cx="0" cy="7" r="1.8" fill="#15803d" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* CLOSED BOOK — hidden until pages fold away */}
|
||||||
|
<g className="sba-closed-book">
|
||||||
|
{/* spine side */}
|
||||||
|
<rect x="55" y="18" width="150" height="140" rx="7" fill="#15803d" />
|
||||||
|
{/* cover face */}
|
||||||
|
<rect x="63" y="18" width="135" height="140" rx="5" fill="#22c55e" />
|
||||||
|
{/* spine shadow */}
|
||||||
|
<rect x="55" y="18" width="10" height="140" rx="4" fill="rgba(0,0,0,0.18)" />
|
||||||
|
{/* decorative ruled lines */}
|
||||||
|
<line x1="83" y1="76" x2="183" y2="76" stroke="rgba(255,255,255,0.22)" strokeWidth="1.5" />
|
||||||
|
<line x1="83" y1="93" x2="183" y2="93" stroke="rgba(255,255,255,0.22)" strokeWidth="1.5" />
|
||||||
|
<line x1="83" y1="110" x2="170" y2="110" stroke="rgba(255,255,255,0.22)" strokeWidth="1.5" />
|
||||||
|
{/* checkmark */}
|
||||||
|
<path className="sba-check" d="M96 90 L115 109 L162 62" stroke="white" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,14 +5,37 @@ import { createEntry } from '../lib/api'
|
|||||||
import { encryptEntry } from '../lib/crypto'
|
import { encryptEntry } from '../lib/crypto'
|
||||||
import BottomNav from '../components/BottomNav'
|
import BottomNav from '../components/BottomNav'
|
||||||
import WelcomeModal from '../components/WelcomeModal'
|
import WelcomeModal from '../components/WelcomeModal'
|
||||||
|
import { SaveBookAnimation } from '../components/SaveBookAnimation'
|
||||||
import { useOnboardingTour, hasSeenOnboarding, markOnboardingDone } from '../hooks/useOnboardingTour'
|
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() {
|
export default function HomePage() {
|
||||||
const { user, userId, secretKey, loading } = useAuth()
|
const { user, userId, secretKey, loading } = useAuth()
|
||||||
const [entry, setEntry] = useState('')
|
const [entry, setEntry] = useState('')
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [phase, setPhase] = useState<'idle' | 'saving' | 'book' | 'celebrate'>('idle')
|
||||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
const [affirmation, setAffirmation] = useState('')
|
||||||
|
const [message, setMessage] = useState<{ type: 'error'; text: string } | null>(null)
|
||||||
const [showWelcome, setShowWelcome] = useState(false)
|
const [showWelcome, setShowWelcome] = useState(false)
|
||||||
|
|
||||||
const titleInputRef = useRef<HTMLInputElement>(null)
|
const titleInputRef = useRef<HTMLInputElement>(null)
|
||||||
@@ -61,6 +84,11 @@ export default function HomePage() {
|
|||||||
.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })
|
.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
|
|
||||||
|
const handleBookDone = () => {
|
||||||
|
setPhase('celebrate')
|
||||||
|
setTimeout(() => setPhase('idle'), 2500)
|
||||||
|
}
|
||||||
|
|
||||||
const handleTitleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleTitleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Enter' && title.trim()) {
|
if (e.key === 'Enter' && title.trim()) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -79,7 +107,7 @@ export default function HomePage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true)
|
setPhase('saving')
|
||||||
setMessage(null)
|
setMessage(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -112,17 +140,14 @@ export default function HomePage() {
|
|||||||
token
|
token
|
||||||
)
|
)
|
||||||
|
|
||||||
setMessage({ type: 'success', text: 'Entry saved securely!' })
|
|
||||||
setTitle('')
|
setTitle('')
|
||||||
setEntry('')
|
setEntry('')
|
||||||
|
setAffirmation(AFFIRMATIONS[Math.floor(Math.random() * AFFIRMATIONS.length)])
|
||||||
// Clear success message after 3 seconds
|
setPhase('book')
|
||||||
setTimeout(() => setMessage(null), 3000)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Failed to save entry'
|
const errorMessage = error instanceof Error ? error.message : 'Failed to save entry'
|
||||||
setMessage({ type: 'error', text: errorMessage })
|
setMessage({ type: 'error', text: errorMessage })
|
||||||
} finally {
|
setPhase('idle')
|
||||||
setSaving(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +173,7 @@ export default function HomePage() {
|
|||||||
onKeyDown={handleTitleKeyDown}
|
onKeyDown={handleTitleKeyDown}
|
||||||
enterKeyHint="next"
|
enterKeyHint="next"
|
||||||
ref={titleInputRef}
|
ref={titleInputRef}
|
||||||
disabled={saving}
|
disabled={phase !== 'idle'}
|
||||||
/>
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
id="tour-content-textarea"
|
id="tour-content-textarea"
|
||||||
@@ -158,7 +183,7 @@ export default function HomePage() {
|
|||||||
onChange={(e) => setEntry(e.target.value)}
|
onChange={(e) => setEntry(e.target.value)}
|
||||||
enterKeyHint="enter"
|
enterKeyHint="enter"
|
||||||
ref={contentTextareaRef}
|
ref={contentTextareaRef}
|
||||||
disabled={saving}
|
disabled={phase !== 'idle'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -168,27 +193,55 @@ export default function HomePage() {
|
|||||||
marginTop: '1rem',
|
marginTop: '1rem',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
backgroundColor: message.type === 'success' ? '#f0fdf4' : '#fef2f2',
|
backgroundColor: '#fef2f2',
|
||||||
color: message.type === 'success' ? '#15803d' : '#b91c1c',
|
color: '#b91c1c',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}>
|
}}>
|
||||||
{message.text}
|
{message.text}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '0.75rem' }}>
|
<div style={{ marginTop: '1.5rem', position: 'relative' }}>
|
||||||
<button
|
{phase === 'celebrate' && (
|
||||||
id="tour-save-btn"
|
<>
|
||||||
className="journal-write-btn"
|
<div className="save-leaves" aria-hidden>
|
||||||
onClick={handleWrite}
|
{SAVE_LEAVES.map((leaf, i) => (
|
||||||
disabled={saving || !title.trim() || !entry.trim()}
|
<span
|
||||||
>
|
key={i}
|
||||||
{saving ? 'Saving...' : 'Save Entry'}
|
className="save-leaf"
|
||||||
</button>
|
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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{phase === 'book' && <SaveBookAnimation onDone={handleBookDone} />}
|
||||||
|
|
||||||
<BottomNav />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user