diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f6f4186..d73b694 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -12,7 +12,9 @@ "Bash(conda run:*)", "Bash(git rm:*)", "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" ] } } diff --git a/src/App.css b/src/App.css index df55149..3049f3f 100644 --- a/src/App.css +++ b/src/App.css @@ -575,6 +575,94 @@ 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 { display: inline-flex; align-items: center; @@ -594,6 +682,142 @@ 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 ============================ */ @@ -2019,6 +2243,13 @@ 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 -- */ [data-theme="dark"] .home-login-link { background: #22c55e; diff --git a/src/components/SaveBookAnimation.tsx b/src/components/SaveBookAnimation.tsx new file mode 100644 index 0000000..5bd0603 --- /dev/null +++ b/src/components/SaveBookAnimation.tsx @@ -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 ( +
+ ) +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 0a5d746..f243d73 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -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