login page update
This commit is contained in:
@@ -11,7 +11,8 @@
|
|||||||
"Bash(/Users/jeet/Library/Python/3.9/bin/pytest -v 2>&1)",
|
"Bash(/Users/jeet/Library/Python/3.9/bin/pytest -v 2>&1)",
|
||||||
"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)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
369
src/App.css
369
src/App.css
@@ -43,24 +43,7 @@
|
|||||||
/* ============================
|
/* ============================
|
||||||
LOGIN PAGE
|
LOGIN PAGE
|
||||||
============================ */
|
============================ */
|
||||||
.login-page {
|
/* ── Loading state ──────────────────────────────────────── */
|
||||||
height: 100dvh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: linear-gradient(160deg, #eef6ee 0%, #dcfce7 100%);
|
|
||||||
padding: 1.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-page__loading {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-page__spinner {
|
.login-page__spinner {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@@ -70,42 +53,141 @@
|
|||||||
animation: spin 0.7s linear infinite;
|
animation: spin 0.7s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card {
|
/* ── Login page — two-panel layout ─────────────────────── */
|
||||||
background: #fff;
|
.lp {
|
||||||
|
height: 100dvh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 55% 45%;
|
||||||
|
overflow: hidden;
|
||||||
|
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;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
gap: 1.25rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__hero-words {
|
||||||
|
text-align: center;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__quote {
|
||||||
|
font-family: "Sniglet", system-ui;
|
||||||
|
font-size: 1.625rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #16a34a;
|
||||||
|
margin: 0 0 0.4rem;
|
||||||
|
opacity: 0;
|
||||||
|
animation: lp-rise 0.9s ease-out 3.0s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__subquote {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: #22c55e;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
animation: lp-rise 0.9s ease-out 3.35s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Right: login panel ─────────────────────────────────── */
|
||||||
|
.lp__panel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
border-radius: 20px;
|
}
|
||||||
border-top: 4px solid #22c55e;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
.lp__form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 360px;
|
max-width: 320px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.75rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24px;
|
||||||
|
border-top: 4px solid #22c55e;
|
||||||
|
padding: 2rem 1.75rem;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__brand {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.625rem;
|
||||||
|
animation: lp-rise 0.7s ease-out 0.1s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__icon {
|
||||||
|
font-size: 3.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
animation: lp-pop 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #22c55e;
|
||||||
|
font-family: "Sniglet", system-ui;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card__brand {
|
.lp__tagline {
|
||||||
margin-bottom: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card__title {
|
|
||||||
margin: 0 0 0.375rem;
|
|
||||||
font-size: 1.625rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #22c55e;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
font-family: "Sniglet", system-ui;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card__tagline {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.65;
|
||||||
|
animation: lp-rise 0.7s ease-out 0.25s both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card__actions {
|
.lp__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
animation: lp-rise 0.7s ease-out 0.4s both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lp__privacy {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__error {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
color: #b91c1c;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
background: #fef2f2;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* kept for other callers */
|
||||||
.login-card__error {
|
.login-card__error {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.625rem 0.75rem;
|
padding: 0.625rem 0.75rem;
|
||||||
@@ -116,6 +198,189 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Tree animation ─────────────────────────────────────── */
|
||||||
|
.tree-wrap {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-svg {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 290px;
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-trunk {
|
||||||
|
stroke-dasharray: 200;
|
||||||
|
stroke-dashoffset: 200;
|
||||||
|
animation: t-draw 0.95s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-root {
|
||||||
|
stroke-dasharray: 120;
|
||||||
|
stroke-dashoffset: 120;
|
||||||
|
animation: t-draw 0.55s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-branch {
|
||||||
|
stroke-dasharray: 300;
|
||||||
|
stroke-dashoffset: 300;
|
||||||
|
animation: t-draw 0.65s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes t-draw {
|
||||||
|
to { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-leaf {
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: center;
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
animation: t-pop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes t-pop {
|
||||||
|
0% { transform: scale(0); opacity: 0; }
|
||||||
|
60% { transform: scale(1.18); opacity: 1; }
|
||||||
|
100% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-canopy {
|
||||||
|
transform-origin: 140px 412px;
|
||||||
|
animation: t-sway 5.5s ease-in-out 3.4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes t-sway {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
28% { transform: rotate(1.8deg); }
|
||||||
|
72% { transform: rotate(-1.5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-particle {
|
||||||
|
opacity: 0;
|
||||||
|
animation: t-float linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes t-float {
|
||||||
|
0% { transform: translateY(0) translateX(0); opacity: 0; }
|
||||||
|
8% { opacity: 0.7; }
|
||||||
|
88% { opacity: 0.35; }
|
||||||
|
100% { transform: translateY(-380px) translateX(28px); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Login animations ───────────────────────────────────── */
|
||||||
|
@keyframes lp-rise {
|
||||||
|
from { opacity: 0; transform: translateY(16px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lp-pop {
|
||||||
|
from { opacity: 0; transform: scale(0.55); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Login responsive — phones (≤ 640px) ───────────────── */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
/* Stack vertically: tree hero on top, login below */
|
||||||
|
.lp {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 56vh 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__hero {
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slightly smaller tree so it fits within the hero panel */
|
||||||
|
.tree-svg {
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__quote {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__subquote {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__panel {
|
||||||
|
padding: 1.25rem 1.25rem calc(1.5rem + env(safe-area-inset-bottom));
|
||||||
|
border-left: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__form {
|
||||||
|
max-width: 420px;
|
||||||
|
gap: 1.25rem;
|
||||||
|
padding: 1.75rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__title {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__tagline {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Login responsive — small phones (≤ 390px, e.g. iPhone SE) */
|
||||||
|
@media (max-width: 390px) {
|
||||||
|
.lp {
|
||||||
|
grid-template-rows: 50vh 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-svg {
|
||||||
|
max-width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__quote {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__subquote {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__panel {
|
||||||
|
padding: 1rem 1rem calc(1.25rem + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__form {
|
||||||
|
padding: 1.5rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__title {
|
||||||
|
font-size: 1.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__form {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp__tagline {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.google-sign-in-btn {
|
.google-sign-in-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1667,9 +1932,7 @@
|
|||||||
color: #4ade80;
|
color: #4ade80;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .login-card__title {
|
/* login page is always light — no dark overrides */
|
||||||
color: #4ade80;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Calendar -- */
|
/* -- Calendar -- */
|
||||||
[data-theme="dark"] .calendar-day {
|
[data-theme="dark"] .calendar-day {
|
||||||
@@ -2001,27 +2264,7 @@
|
|||||||
background: #333;
|
background: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -- Login page -- */
|
/* -- Login page — light mode only, no dark theme overrides -- */
|
||||||
[data-theme="dark"] .login-page {
|
|
||||||
background: linear-gradient(160deg, #0f0f0f 0%, #0a2e14 50%, #0f0f0f 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .login-card {
|
|
||||||
background: #1a1a1a;
|
|
||||||
border-top-color: #4ade80;
|
|
||||||
box-shadow:
|
|
||||||
0 8px 32px rgba(0, 0, 0, 0.5),
|
|
||||||
0 0 0 1px rgba(74, 222, 128, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .login-card__tagline {
|
|
||||||
color: #7a8a7a;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .login-card__error {
|
|
||||||
background: rgba(239, 68, 68, 0.1);
|
|
||||||
color: #f87171;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- Google sign-in btn -- */
|
/* -- Google sign-in btn -- */
|
||||||
[data-theme="dark"] .google-sign-in-btn {
|
[data-theme="dark"] .google-sign-in-btn {
|
||||||
|
|||||||
153
src/components/TreeAnimation.tsx
Normal file
153
src/components/TreeAnimation.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
const LEAVES = [
|
||||||
|
// Left low cluster (b1 tip ~40,308)
|
||||||
|
{ cx: 34, cy: 302, r: 18, fill: '#22c55e', delay: '1.65s' },
|
||||||
|
{ cx: 14, cy: 295, r: 15, fill: '#16a34a', delay: '1.70s' },
|
||||||
|
{ cx: 26, cy: 280, r: 16, fill: '#4ade80', delay: '1.68s' },
|
||||||
|
{ cx: 48, cy: 290, r: 13, fill: '#15803d', delay: '1.72s' },
|
||||||
|
{ cx: 8, cy: 312, r: 12, fill: '#22c55e', delay: '1.75s' },
|
||||||
|
// Right low cluster (b2 tip ~240,302)
|
||||||
|
{ cx: 246, cy: 296, r: 18, fill: '#22c55e', delay: '1.75s' },
|
||||||
|
{ cx: 266, cy: 290, r: 15, fill: '#16a34a', delay: '1.80s' },
|
||||||
|
{ cx: 254, cy: 275, r: 16, fill: '#4ade80', delay: '1.78s' },
|
||||||
|
{ cx: 234, cy: 286, r: 13, fill: '#15803d', delay: '1.82s' },
|
||||||
|
{ cx: 270, cy: 308, r: 12, fill: '#22c55e', delay: '1.85s' },
|
||||||
|
// sb3/sb4 mid-tips
|
||||||
|
{ cx: 50, cy: 270, r: 13, fill: '#4ade80', delay: '1.80s' },
|
||||||
|
{ cx: 228, cy: 267, r: 13, fill: '#4ade80', delay: '1.85s' },
|
||||||
|
// sb1/sb2 outer tips
|
||||||
|
{ cx: 8, cy: 255, r: 14, fill: '#4ade80', delay: '1.90s' },
|
||||||
|
{ cx: 270, cy: 251, r: 14, fill: '#4ade80', delay: '1.90s' },
|
||||||
|
// Left mid cluster (b3 tip ~44,258)
|
||||||
|
{ cx: 38, cy: 252, r: 16, fill: '#22c55e', delay: '2.05s' },
|
||||||
|
{ cx: 18, cy: 246, r: 13, fill: '#4ade80', delay: '2.10s' },
|
||||||
|
{ cx: 30, cy: 232, r: 14, fill: '#16a34a', delay: '2.08s' },
|
||||||
|
{ cx: 52, cy: 240, r: 11, fill: '#86efac', delay: '2.12s' },
|
||||||
|
{ cx: 12, cy: 264, r: 10, fill: '#22c55e', delay: '2.15s' },
|
||||||
|
// Right mid cluster (b4 tip ~236,255)
|
||||||
|
{ cx: 242, cy: 248, r: 16, fill: '#22c55e', delay: '2.10s' },
|
||||||
|
{ cx: 262, cy: 242, r: 13, fill: '#4ade80', delay: '2.15s' },
|
||||||
|
{ cx: 250, cy: 228, r: 14, fill: '#16a34a', delay: '2.12s' },
|
||||||
|
{ cx: 230, cy: 238, r: 11, fill: '#86efac', delay: '2.18s' },
|
||||||
|
{ cx: 266, cy: 260, r: 10, fill: '#22c55e', delay: '2.20s' },
|
||||||
|
// sb5/sb6 outer tips (~16,214 and ~262,210)
|
||||||
|
{ cx: 12, cy: 208, r: 13, fill: '#86efac', delay: '2.30s' },
|
||||||
|
{ cx: 266, cy: 206, r: 13, fill: '#86efac', delay: '2.30s' },
|
||||||
|
// Left upper cluster (b5 tip ~86,218)
|
||||||
|
{ cx: 80, cy: 212, r: 17, fill: '#4ade80', delay: '2.45s' },
|
||||||
|
{ cx: 62, cy: 202, r: 14, fill: '#22c55e', delay: '2.50s' },
|
||||||
|
{ cx: 90, cy: 196, r: 12, fill: '#86efac', delay: '2.48s' },
|
||||||
|
{ cx: 68, cy: 188, r: 13, fill: '#4ade80', delay: '2.52s' },
|
||||||
|
// Right upper cluster (b6 tip ~194,214)
|
||||||
|
{ cx: 200, cy: 208, r: 17, fill: '#4ade80', delay: '2.48s' },
|
||||||
|
{ cx: 218, cy: 198, r: 14, fill: '#22c55e', delay: '2.52s' },
|
||||||
|
{ cx: 192, cy: 193, r: 12, fill: '#86efac', delay: '2.50s' },
|
||||||
|
{ cx: 210, cy: 185, r: 13, fill: '#4ade80', delay: '2.55s' },
|
||||||
|
// Top center canopy (b7 tip ~128,196)
|
||||||
|
{ cx: 120, cy: 188, r: 16, fill: '#4ade80', delay: '2.60s' },
|
||||||
|
{ cx: 140, cy: 176, r: 21, fill: '#22c55e', delay: '2.65s' },
|
||||||
|
{ cx: 160, cy: 188, r: 16, fill: '#4ade80', delay: '2.62s' },
|
||||||
|
{ cx: 126, cy: 166, r: 13, fill: '#16a34a', delay: '2.68s' },
|
||||||
|
{ cx: 154, cy: 164, r: 14, fill: '#86efac', delay: '2.72s' },
|
||||||
|
{ cx: 140, cy: 154, r: 18, fill: '#22c55e', delay: '2.75s' },
|
||||||
|
{ cx: 134, cy: 142, r: 12, fill: '#4ade80', delay: '2.78s' },
|
||||||
|
{ cx: 148, cy: 140, r: 11, fill: '#86efac', delay: '2.80s' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const PARTICLES = [
|
||||||
|
{ cx: 45, cy: 420, r: 5, fill: '#4ade80', delay: '3.5s', dur: '7s' },
|
||||||
|
{ cx: 235, cy: 415, r: 3, fill: '#86efac', delay: '5.0s', dur: '9s' },
|
||||||
|
{ cx: 88, cy: 425, r: 4, fill: '#22c55e', delay: '4.0s', dur: '8s' },
|
||||||
|
{ cx: 192, cy: 418, r: 5, fill: '#4ade80', delay: '6.0s', dur: '10s' },
|
||||||
|
{ cx: 140, cy: 422, r: 3, fill: '#86efac', delay: '3.8s', dur: '6s' },
|
||||||
|
{ cx: 115, cy: 416, r: 4, fill: '#22c55e', delay: '7.0s', dur: '8s' },
|
||||||
|
{ cx: 165, cy: 424, r: 3, fill: '#4ade80', delay: '4.5s', dur: '7s' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function TreeAnimation() {
|
||||||
|
return (
|
||||||
|
<div className="tree-wrap">
|
||||||
|
<svg
|
||||||
|
className="tree-svg"
|
||||||
|
viewBox="0 115 280 325"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{/* Floating leaf particles */}
|
||||||
|
{PARTICLES.map((p, i) => (
|
||||||
|
<circle
|
||||||
|
key={i}
|
||||||
|
className="t-particle"
|
||||||
|
cx={p.cx} cy={p.cy} r={p.r} fill={p.fill}
|
||||||
|
style={{ animationDelay: p.delay, animationDuration: p.dur }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Roots */}
|
||||||
|
<path className="t-root" style={{ animationDelay: '1.00s' }}
|
||||||
|
d="M 134 408 C 108 414 80 412 56 418" stroke="#C4954A" strokeWidth="5" strokeLinecap="round" />
|
||||||
|
<path className="t-root" style={{ animationDelay: '1.05s' }}
|
||||||
|
d="M 146 408 C 172 414 200 412 224 418" stroke="#C4954A" strokeWidth="5" strokeLinecap="round" />
|
||||||
|
<path className="t-root" style={{ animationDelay: '1.02s' }}
|
||||||
|
d="M 140 410 C 138 422 134 430 128 436" stroke="#C4954A" strokeWidth="4" strokeLinecap="round" />
|
||||||
|
<path className="t-root" style={{ animationDelay: '1.08s' }}
|
||||||
|
d="M 140 410 C 142 422 146 430 152 436" stroke="#C4954A" strokeWidth="4" strokeLinecap="round" />
|
||||||
|
|
||||||
|
{/* Trunk — two overlapping strokes for depth */}
|
||||||
|
<path className="t-trunk" style={{ animationDelay: '0.20s' }}
|
||||||
|
d="M 133 410 L 133 265" stroke="#8B6120" strokeWidth="17" strokeLinecap="round" />
|
||||||
|
<path className="t-trunk" style={{ animationDelay: '0.28s' }}
|
||||||
|
d="M 147 410 L 147 265" stroke="#C4954A" strokeWidth="7" strokeLinecap="round" />
|
||||||
|
|
||||||
|
{/* Level-1 branches */}
|
||||||
|
<path className="t-branch" style={{ animationDelay: '1.00s' }}
|
||||||
|
d="M 136 356 C 104 336 70 322 40 308" stroke="#A0732A" strokeWidth="8" strokeLinecap="round" />
|
||||||
|
<path className="t-branch" style={{ animationDelay: '1.10s' }}
|
||||||
|
d="M 144 348 C 176 328 210 314 240 302" stroke="#A0732A" strokeWidth="8" strokeLinecap="round" />
|
||||||
|
|
||||||
|
{/* Level-2 branches */}
|
||||||
|
<path className="t-branch" style={{ animationDelay: '1.50s' }}
|
||||||
|
d="M 136 310 C 104 292 70 276 44 258" stroke="#9B6D28" strokeWidth="6" strokeLinecap="round" />
|
||||||
|
<path className="t-branch" style={{ animationDelay: '1.60s' }}
|
||||||
|
d="M 144 304 C 176 286 210 270 236 255" stroke="#9B6D28" strokeWidth="6" strokeLinecap="round" />
|
||||||
|
|
||||||
|
{/* Level-3 branches */}
|
||||||
|
<path className="t-branch" style={{ animationDelay: '1.90s' }}
|
||||||
|
d="M 136 272 C 115 253 100 237 86 218" stroke="#9B6D28" strokeWidth="5" strokeLinecap="round" />
|
||||||
|
<path className="t-branch" style={{ animationDelay: '2.00s' }}
|
||||||
|
d="M 144 268 C 165 249 180 233 194 214" stroke="#9B6D28" strokeWidth="5" strokeLinecap="round" />
|
||||||
|
<path className="t-branch" style={{ animationDelay: '2.10s' }}
|
||||||
|
d="M 140 252 C 136 232 132 215 128 196" stroke="#9B6D28" strokeWidth="4" strokeLinecap="round" />
|
||||||
|
|
||||||
|
{/* Sub-branches off level-1 */}
|
||||||
|
<path className="t-branch" style={{ animationDelay: '1.55s' }}
|
||||||
|
d="M 40 308 C 24 292 16 276 12 260" stroke="#8B6520" strokeWidth="4" strokeLinecap="round" />
|
||||||
|
<path className="t-branch" style={{ animationDelay: '1.65s' }}
|
||||||
|
d="M 240 302 C 256 286 262 270 266 255" stroke="#8B6520" strokeWidth="4" strokeLinecap="round" />
|
||||||
|
<path className="t-branch" style={{ animationDelay: '1.45s' }}
|
||||||
|
d="M 74 326 C 60 308 54 292 52 276" stroke="#8B6520" strokeWidth="3" strokeLinecap="round" />
|
||||||
|
<path className="t-branch" style={{ animationDelay: '1.55s' }}
|
||||||
|
d="M 206 320 C 220 302 224 286 224 271" stroke="#8B6520" strokeWidth="3" strokeLinecap="round" />
|
||||||
|
|
||||||
|
{/* Sub-branches off level-2 */}
|
||||||
|
<path className="t-branch" style={{ animationDelay: '2.05s' }}
|
||||||
|
d="M 44 258 C 28 242 20 228 16 214" stroke="#8B6520" strokeWidth="3" strokeLinecap="round" />
|
||||||
|
<path className="t-branch" style={{ animationDelay: '2.15s' }}
|
||||||
|
d="M 236 255 C 252 239 258 225 262 210" stroke="#8B6520" strokeWidth="3" strokeLinecap="round" />
|
||||||
|
|
||||||
|
{/* Leaves — inside a group so the whole canopy can sway */}
|
||||||
|
<g className="t-canopy">
|
||||||
|
{LEAVES.map((l, i) => (
|
||||||
|
<circle
|
||||||
|
key={i}
|
||||||
|
className="t-leaf"
|
||||||
|
cx={l.cx} cy={l.cy} r={l.r}
|
||||||
|
fill={l.fill}
|
||||||
|
style={{ animationDelay: l.delay }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { useAuth } from '../contexts/AuthContext'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { GoogleSignInButton } from '../components/GoogleSignInButton'
|
import { GoogleSignInButton } from '../components/GoogleSignInButton'
|
||||||
import { LoginCard } from '../components/LoginCard'
|
import { TreeAnimation } from '../components/TreeAnimation'
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const { user, loading, signInWithGoogle } = useAuth()
|
const { user, loading, signInWithGoogle } = useAuth()
|
||||||
@@ -12,9 +12,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) return
|
if (loading) return
|
||||||
if (user) {
|
if (user) navigate('/', { replace: true })
|
||||||
navigate('/', { replace: true })
|
|
||||||
}
|
|
||||||
}, [user, loading, navigate])
|
}, [user, loading, navigate])
|
||||||
|
|
||||||
async function handleGoogleSignIn() {
|
async function handleGoogleSignIn() {
|
||||||
@@ -31,31 +29,46 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="login-page">
|
<div className="lp lp--loading" aria-live="polite">
|
||||||
<div className="login-page__loading" aria-live="polite">
|
|
||||||
<span className="login-page__spinner" aria-hidden />
|
<span className="login-page__spinner" aria-hidden />
|
||||||
<p>Loading…</p>
|
<p>Loading…</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-page">
|
<div className="lp">
|
||||||
<LoginCard
|
{/* ── Left: animated tree hero ─────────────────── */}
|
||||||
title="Grateful Journal"
|
<div className="lp__hero">
|
||||||
tagline="A minimal, private space for gratitude and reflection. No feeds, no noise—just you and your thoughts."
|
<TreeAnimation />
|
||||||
>
|
<div className="lp__hero-words">
|
||||||
<GoogleSignInButton
|
<p className="lp__quote">Grow your gratitude.</p>
|
||||||
loading={signingIn}
|
<p className="lp__subquote">One small moment at a time.</p>
|
||||||
onClick={handleGoogleSignIn}
|
</div>
|
||||||
/>
|
</div>
|
||||||
{error && (
|
|
||||||
<p className="login-card__error" role="alert">
|
{/* ── Right: login panel ───────────────────────── */}
|
||||||
{error}
|
<div className="lp__panel">
|
||||||
|
<div className="lp__form">
|
||||||
|
<div className="lp__brand">
|
||||||
|
<span className="lp__icon" aria-hidden>🌱</span>
|
||||||
|
<h1 className="lp__title">Grateful Journal</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="lp__tagline">
|
||||||
|
A private space for gratitude and reflection.<br />
|
||||||
|
No feeds. No noise. Just you and your thoughts.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="lp__actions">
|
||||||
|
<GoogleSignInButton loading={signingIn} onClick={handleGoogleSignIn} />
|
||||||
|
<p className="lp__privacy">🔒 End-to-end encrypted. We never read your entries.</p>
|
||||||
|
{error && (
|
||||||
|
<p className="lp__error" role="alert">{error}</p>
|
||||||
)}
|
)}
|
||||||
</LoginCard>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
38
start-all.sh
38
start-all.sh
@@ -5,47 +5,57 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "🚀 Starting Grateful Journal..."
|
# Cleanup on Ctrl+C or exit
|
||||||
|
cleanup() {
|
||||||
|
echo ""
|
||||||
|
echo "Stopping all services..."
|
||||||
|
kill $BACKEND_PID $FRONTEND_PID 2>/dev/null || true
|
||||||
|
wait $BACKEND_PID $FRONTEND_PID 2>/dev/null || true
|
||||||
|
echo "All services stopped."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap cleanup INT TERM
|
||||||
|
|
||||||
|
echo "Starting Grateful Journal..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check if MongoDB is running
|
# Check if MongoDB is running
|
||||||
echo "📦 Checking MongoDB..."
|
echo "Checking MongoDB..."
|
||||||
if lsof -Pi :27017 -sTCP:LISTEN -t >/dev/null 2>&1 ; then
|
if lsof -Pi :27017 -sTCP:LISTEN -t >/dev/null 2>&1 ; then
|
||||||
echo "✓ MongoDB already running on port 27017"
|
echo "MongoDB already running on port 27017"
|
||||||
else
|
else
|
||||||
echo "Starting MongoDB..."
|
echo "Starting MongoDB..."
|
||||||
brew services start mongodb-community
|
brew services start mongodb-community
|
||||||
sleep 2
|
sleep 2
|
||||||
echo "✓ MongoDB started on port 27017"
|
echo "MongoDB started on port 27017"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Start Backend (FastAPI with conda environment)
|
# Start Backend (FastAPI with conda environment)
|
||||||
echo "🔄 Starting FastAPI backend..."
|
echo "Starting FastAPI backend..."
|
||||||
# Activate conda and start backend
|
|
||||||
conda run -n yoyo python backend/main.py &
|
conda run -n yoyo python backend/main.py &
|
||||||
BACKEND_PID=$!
|
BACKEND_PID=$!
|
||||||
echo "✓ Backend running on http://localhost:8001 (PID: $BACKEND_PID)"
|
echo "Backend running on http://localhost:8001 (PID: $BACKEND_PID)"
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Start Frontend (Vite)
|
# Start Frontend (Vite)
|
||||||
echo "🔄 Starting Vite frontend..."
|
echo "Starting Vite frontend..."
|
||||||
npm run dev -- --port 8000 &
|
npm run dev -- --port 8000 &
|
||||||
FRONTEND_PID=$!
|
FRONTEND_PID=$!
|
||||||
echo "✓ Frontend running on http://localhost:8000 (PID: $FRONTEND_PID)"
|
echo "Frontend running on http://localhost:8000 (PID: $FRONTEND_PID)"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ All services started!"
|
echo "All services started!"
|
||||||
echo ""
|
echo ""
|
||||||
echo "📱 Frontend: http://localhost:8000"
|
echo "Frontend: http://localhost:8000"
|
||||||
echo "🔌 Backend: http://localhost:8001"
|
echo "Backend: http://localhost:8001"
|
||||||
echo "📄 API Docs: http://localhost:8001/docs"
|
echo "API Docs: http://localhost:8001/docs"
|
||||||
echo ""
|
echo ""
|
||||||
echo "To stop all services, press Ctrl+C"
|
echo "Press Ctrl+C to stop all services"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Wait for both processes
|
# Wait for both processes
|
||||||
|
|||||||
Reference in New Issue
Block a user