diff --git a/frontend/src/App.css b/frontend/src/App.css index 888958f..fe7c1e1 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -104,7 +104,9 @@ position: sticky; top: 0; z-index: 100; - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04), 0 4px 24px rgba(0, 0, 0, 0.4); + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.04), + 0 4px 24px rgba(0, 0, 0, 0.4); } .header-left { @@ -187,7 +189,10 @@ display: flex; align-items: center; justify-content: center; - transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease; + transition: + border-color 0.2s ease, + box-shadow 0.2s ease, + transform 0.15s ease; } .avatar-upload-wrapper:hover { @@ -249,6 +254,78 @@ color: var(--bear-red); } +@media (max-width: 768px) { + .dashboard-header { + padding: 0 12px; + } + + .header-left { + gap: 6px; + flex-shrink: 0; + } + + .logo-text { + font-size: 17px; + } + + .header-right { + gap: 8px; + min-width: 0; + } + + .credits-badge { + gap: 3px; + padding: 3px 5px; + font-size: 9px; + } + + .recharge-btn { + padding: 3px 5px; + font-size: 8px; + } + + .user-profile { + gap: 0; + } + + .profile-name { + display: none; + } + + .avatar-upload-wrapper, + .logout-btn { + width: 36px; + height: 36px; + flex-shrink: 0; + } +} + +@media (min-width: 375px) and (max-width: 480px) { + .credits-badge { + gap: 5px; + padding: 5px 8px; + font-size: 11px; + } + + .credits-badge svg { + width: 16px; + height: 16px; + } +} + +@media (min-width: 481px) and (max-width: 768px) { + .credits-badge { + gap: 6px; + padding: 6px 9px; + font-size: 12px; + } + + .credits-badge svg { + width: 17px; + height: 17px; + } +} + /* ─── 3. Dashboard Grid ─────────────────── */ .dashboard-grid { display: grid; @@ -275,7 +352,8 @@ .dashboard-grid { grid-template-columns: 1fr; } - .left-sidebar, .right-sidebar { + .left-sidebar, + .right-sidebar { grid-column: span 1; } } @@ -287,7 +365,9 @@ border-radius: 14px; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); - transition: border-color 0.3s ease, box-shadow 0.3s ease; + transition: + border-color 0.3s ease, + box-shadow 0.3s ease; overflow: hidden; } @@ -371,7 +451,7 @@ overflow-y: auto; padding: 8px; scrollbar-width: thin; - scrollbar-color: rgba(255,255,255,0.06) transparent; + scrollbar-color: rgba(255, 255, 255, 0.06) transparent; } .watchlist-item { @@ -389,7 +469,7 @@ } .watchlist-item::before { - content: ''; + content: ""; position: absolute; left: 0; top: 0; @@ -462,7 +542,7 @@ overflow-y: auto; max-height: calc(100vh - 130px); scrollbar-width: thin; - scrollbar-color: rgba(255,255,255,0.06) transparent; + scrollbar-color: rgba(255, 255, 255, 0.06) transparent; } /* Chart Styles */ @@ -588,7 +668,9 @@ border-radius: 99px; box-shadow: 0 0 20px rgba(161, 84, 255, 0.3); letter-spacing: 0.01em; - transition: box-shadow 0.25s ease, transform 0.15s ease; + transition: + box-shadow 0.25s ease, + transform 0.15s ease; } .insight-btn:hover { @@ -619,7 +701,9 @@ } @keyframes spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } .insight-result { @@ -812,7 +896,7 @@ flex-direction: column; gap: 8px; scrollbar-width: thin; - scrollbar-color: rgba(255,255,255,0.06) transparent; + scrollbar-color: rgba(255, 255, 255, 0.06) transparent; } .alert-item { @@ -976,24 +1060,48 @@ /* ─── 9. Animations ─────────────────────── */ @keyframes slideUp { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } @keyframes pulseGlow { - 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(0, 242, 254, 0.7); } - 70% { transform: scale(1.2); box-shadow: 0 0 0 5px rgba(0, 242, 254, 0); } - 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(0, 242, 254, 0); } + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(0, 242, 254, 0.7); + } + 70% { + transform: scale(1.2); + box-shadow: 0 0 0 5px rgba(0, 242, 254, 0); + } + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(0, 242, 254, 0); + } } @keyframes pulsePill { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.7; + } } .mini-chart-container, @@ -1002,8 +1110,14 @@ } @keyframes fadeInChart { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } } .animate-fade { @@ -1011,8 +1125,12 @@ } /* Neon glow filter helpers */ -.glow-cyan { filter: drop-shadow(0 0 6px rgba(0, 242, 254, 0.7)); } -.glow-violet { filter: drop-shadow(0 0 6px rgba(161, 84, 255, 0.7)); } +.glow-cyan { + filter: drop-shadow(0 0 6px rgba(0, 242, 254, 0.7)); +} +.glow-violet { + filter: drop-shadow(0 0 6px rgba(161, 84, 255, 0.7)); +} /* ─── 10. Landing Page Styles ───────────── */ .landing-page-container { @@ -1027,7 +1145,10 @@ .bg-glow-mesh { position: absolute; - top: 0; left: 0; right: 0; bottom: 0; + top: 0; + left: 0; + right: 0; + bottom: 0; pointer-events: none; z-index: 0; overflow: hidden; @@ -1041,27 +1162,41 @@ } .mesh-cyan { - width: 400px; height: 400px; + width: 400px; + height: 400px; background: var(--neon-cyan); - top: -100px; left: 10%; + top: -100px; + left: 10%; animation: floatLight 20s infinite ease-in-out; } .mesh-violet { - width: 500px; height: 500px; + width: 500px; + height: 500px; background: var(--neon-violet); - bottom: -150px; right: 10%; + bottom: -150px; + right: 10%; animation: floatLight2 25s infinite ease-in-out; } @keyframes floatLight { - 0%, 100% { transform: translate(0, 0) scale(1); } - 50% { transform: translate(50px, 30px) scale(1.1); } + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(50px, 30px) scale(1.1); + } } @keyframes floatLight2 { - 0%, 100% { transform: translate(0, 0) scale(1.1); } - 50% { transform: translate(-40px, -60px) scale(0.9); } + 0%, + 100% { + transform: translate(0, 0) scale(1.1); + } + 50% { + transform: translate(-40px, -60px) scale(0.9); + } } .landing-header { @@ -1145,7 +1280,9 @@ } @media (max-width: 640px) { - .hero-title { font-size: 36px; } + .hero-title { + font-size: 36px; + } } .hero-subtitle { @@ -1181,9 +1318,15 @@ border-radius: 50%; } -.mockup-dots .red { background: #ff5f56; } -.mockup-dots .yellow { background: #ffbd2e; } -.mockup-dots .green { background: #27c93f; } +.mockup-dots .red { + background: #ff5f56; +} +.mockup-dots .yellow { + background: #ffbd2e; +} +.mockup-dots .green { + background: #27c93f; +} .mockup-url { margin: 0 auto; @@ -1333,7 +1476,7 @@ .auth-divider::before, .auth-divider::after { - content: ''; + content: ""; flex: 1; height: 1px; background: rgba(255, 255, 255, 0.08); @@ -1399,7 +1542,9 @@ } @media (max-width: 768px) { - .features-grid { grid-template-columns: 1fr; } + .features-grid { + grid-template-columns: 1fr; + } } .feature-card { @@ -1599,8 +1744,12 @@ } @keyframes scrollTicker { - 0% { transform: translate3d(0, 0, 0); } - 100% { transform: translate3d(-50%, 0, 0); } + 0% { + transform: translate3d(0, 0, 0); + } + 100% { + transform: translate3d(-50%, 0, 0); + } } /* Stock Key Metrics Grid */ @@ -1677,7 +1826,9 @@ padding: 24px; border-radius: 16px; background: rgba(13, 16, 27, 0.85); - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), inset 0 1px 1px rgba(255, 255, 255, 0.1); + box-shadow: + 0 20px 50px rgba(0, 0, 0, 0.5), + inset 0 1px 1px rgba(255, 255, 255, 0.1); text-align: left; } @@ -1726,7 +1877,9 @@ /* Pricing & SaaS Plans Recharge Modal Styles */ .pricing-modal-content { background: rgba(10, 12, 22, 0.9) !important; - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.7), inset 0 1px 1px rgba(255, 255, 255, 0.05) !important; + box-shadow: + 0 24px 64px rgba(0, 0, 0, 0.7), + inset 0 1px 1px rgba(255, 255, 255, 0.05) !important; } .pricing-card { @@ -1752,7 +1905,8 @@ } @keyframes pulse-cyan-glow { - 0%, 100% { + 0%, + 100% { border-color: rgba(0, 242, 254, 0.4); box-shadow: 0 8px 32px rgba(0, 242, 254, 0.1); } @@ -1876,7 +2030,9 @@ background: rgba(239, 68, 68, 0.08); border: 1px solid rgba(239, 68, 68, 0.3); color: #ef4444; - animation: slide-in-toast 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards, shake-toast 0.4s ease-in-out 0.3s; + animation: + slide-in-toast 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards, + shake-toast 0.4s ease-in-out 0.3s; } @keyframes slide-in-toast { @@ -1891,9 +2047,16 @@ } @keyframes shake-toast { - 0%, 100% { transform: translateX(0); } - 25% { transform: translateX(-4px); } - 75% { transform: translateX(4px); } + 0%, + 100% { + transform: translateX(0); + } + 25% { + transform: translateX(-4px); + } + 75% { + transform: translateX(4px); + } } /* Indicators Controls Styles */ @@ -1997,8 +2160,8 @@ input::-webkit-inner-spin-button { } /* Reveal arrows when the input field is hovered */ -input[type=number]:hover::-webkit-outer-spin-button, -input[type=number]:hover::-webkit-inner-spin-button { +input[type="number"]:hover::-webkit-outer-spin-button, +input[type="number"]:hover::-webkit-inner-spin-button { opacity: 0.7 !important; } @@ -2009,7 +2172,7 @@ input::-webkit-inner-spin-button:hover { filter: invert(1) brightness(1.1) !important; } -input[type=number] { +input[type="number"] { -moz-appearance: number-input !important; } @@ -2160,7 +2323,6 @@ input[type=number] { transform: rotateX(60deg) translateZ(-10px); } - /* ─── The pile container tilted in 3D ─────── */ .book-pile { position: relative; @@ -2176,7 +2338,6 @@ input[type=number] { transform: rotateX(38deg) rotateY(-4deg) rotateZ(-8deg) translateY(-10px); } - /* ─── Volumetric 3D Book Container ─────────── */ .book-3d { position: absolute; @@ -2186,23 +2347,47 @@ input[type=number] { transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); } - /* ─── Per-book thickness variable & Z-positions ─ Each book sits directly on top of the one below. Z-offset = sum of thicknesses of all books below. ──────────────────────────────────────────────── */ -.book-3d.book-bottom { --t: 46px; transform: translateZ(0px); } -.book-3d.book-4 { --t: 42px; transform: translateZ(46px) translateX(-3px); } -.book-3d.book-3 { --t: 44px; transform: translateZ(88px) translateX(3px); } -.book-3d.book-2 { --t: 44px; transform: translateZ(132px) translateX(-2px); } -.book-3d.book-top { --t: 42px; transform: translateZ(176px); } +.book-3d.book-bottom { + --t: 46px; + transform: translateZ(0px); +} +.book-3d.book-4 { + --t: 42px; + transform: translateZ(46px) translateX(-3px); +} +.book-3d.book-3 { + --t: 44px; + transform: translateZ(88px) translateX(3px); +} +.book-3d.book-2 { + --t: 44px; + transform: translateZ(132px) translateX(-2px); +} +.book-3d.book-top { + --t: 42px; + transform: translateZ(176px); +} /* ─── Hover: fan books out slightly ──────────── */ -.book-stack-wrapper:hover .book-3d.book-bottom { transform: translateZ(0px) translateX(-6px); } -.book-stack-wrapper:hover .book-3d.book-4 { transform: translateZ(46px) translateX(-12px) translateY(2px); } -.book-stack-wrapper:hover .book-3d.book-3 { transform: translateZ(88px) translateX(6px) translateY(4px); } -.book-stack-wrapper:hover .book-3d.book-2 { transform: translateZ(132px) translateX(-4px) translateY(-2px); } -.book-stack-wrapper:hover .book-3d.book-top { transform: translateZ(176px) translateX(-2px) translateY(-10px); } +.book-stack-wrapper:hover .book-3d.book-bottom { + transform: translateZ(0px) translateX(-6px); +} +.book-stack-wrapper:hover .book-3d.book-4 { + transform: translateZ(46px) translateX(-12px) translateY(2px); +} +.book-stack-wrapper:hover .book-3d.book-3 { + transform: translateZ(88px) translateX(6px) translateY(4px); +} +.book-stack-wrapper:hover .book-3d.book-2 { + transform: translateZ(132px) translateX(-4px) translateY(-2px); +} +.book-stack-wrapper:hover .book-3d.book-top { + transform: translateZ(176px) translateX(-2px) translateY(-10px); +} /* ─── Top book flies open on click ─────────── */ .book-3d.book-top.is-opening { @@ -2215,7 +2400,6 @@ input[type=number] { is pixel-perfectly aligned to its covers. ─────────────────────────────────────────── */ - /* Back cover (Z = 0, faces away from viewer) */ .book-face-bottom { position: absolute; @@ -2232,8 +2416,8 @@ input[type=number] { background: var(--cover-gradient); border-radius: 3px 6px 6px 3px; box-shadow: - inset 0 0 30px rgba(0,0,0,0.45), - inset 4px 0 10px rgba(255,255,255,0.06), + inset 0 0 30px rgba(0, 0, 0, 0.45), + inset 4px 0 10px rgba(255, 255, 255, 0.06), /* Soft cosmic ambient highlight + warm gold light reflection overlay */ inset 0 15px 30px rgba(14, 165, 233, 0.08), inset 0 -20px 40px rgba(251, 191, 36, 0.09); @@ -2264,8 +2448,8 @@ input[type=number] { transform: rotateY(-90deg); background: var(--spine-gradient); box-shadow: - inset -4px 0 16px rgba(0,0,0,0.75), - inset 2px 0 6px rgba(255,255,255,0.05); + inset -4px 0 16px rgba(0, 0, 0, 0.75), + inset 2px 0 6px rgba(255, 255, 255, 0.05); display: flex; flex-direction: column; justify-content: space-around; @@ -2288,7 +2472,12 @@ input[type=number] { background-color: #f4ebd7; /* Solid fallback to prevent transparency gaps */ background: /* Warm gold ambient highlight blend + deep ambient shading */ - linear-gradient(to right, rgba(251, 191, 36, 0.15) 0%, transparent 40%, rgba(0,0,0,0.2) 100%), + linear-gradient( + to right, + rgba(251, 191, 36, 0.15) 0%, + transparent 40%, + rgba(0, 0, 0, 0.2) 100% + ), repeating-linear-gradient( to bottom, #f5e9d3 0px, @@ -2297,7 +2486,7 @@ input[type=number] { #e8dbbe 3px, #f5e9d3 4px ); - box-shadow: inset 4px 0 10px rgba(0,0,0,0.3); + box-shadow: inset 4px 0 10px rgba(0, 0, 0, 0.3); } /* Bottom / fore-edge page block @@ -2315,7 +2504,12 @@ input[type=number] { background-color: #f4ebd7; /* Solid fallback */ background: /* Warm gold reflection overlay + deep ambient shading */ - linear-gradient(to bottom, rgba(251, 191, 36, 0.15) 0%, transparent 40%, rgba(0,0,0,0.2) 100%), + linear-gradient( + to bottom, + rgba(251, 191, 36, 0.15) 0%, + transparent 40%, + rgba(0, 0, 0, 0.2) 100% + ), repeating-linear-gradient( to right, #f5e9d3 0px, @@ -2324,12 +2518,9 @@ input[type=number] { #e8dbbe 3px, #f5e9d3 4px ); - box-shadow: inset 0 3px 8px rgba(0,0,0,0.25); + box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.25); } - - - /* ─── Spine Decoration ──────────────────────── */ .book-spine-ridges { position: absolute; @@ -2344,19 +2535,19 @@ input[type=number] { .book-spine-ridge { width: 100%; height: 3px; - background: rgba(0,0,0,0.45); + background: rgba(0, 0, 0, 0.45); border-top: 1px solid rgba(191, 149, 63, 0.5); - border-bottom: 1px solid rgba(255,255,255,0.06); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); } .book-spine-text { - font-family: 'EB Garamond', serif; + font-family: "EB Garamond", serif; font-size: 10px; font-weight: 700; letter-spacing: 2.5px; text-transform: uppercase; color: #fcebc4; - text-shadow: 0 1px 4px rgba(0,0,0,0.95); + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.95); white-space: nowrap; transform: rotate(90deg); transform-origin: center; @@ -2372,10 +2563,24 @@ input[type=number] { /* ─── 3D Opening Keyframe ────────────────────── */ @keyframes bookFlyOpen3D { - 0% { transform: translateZ(144px) rotateX(0deg) rotateZ(0deg); opacity: 1; } - 40% { transform: translateZ(280px) rotateX(-25deg) rotateZ(-10deg) translateY(-30px); opacity: 1; } - 70% { transform: translateZ(420px) rotateX(-55deg) rotateZ(5deg) translateY(-80px) scale(1.05); opacity: 0.8; } - 100% { transform: translateZ(560px) rotateX(-90deg) translateY(-160px) scale(1.2); opacity: 0; } + 0% { + transform: translateZ(144px) rotateX(0deg) rotateZ(0deg); + opacity: 1; + } + 40% { + transform: translateZ(280px) rotateX(-25deg) rotateZ(-10deg) + translateY(-30px); + opacity: 1; + } + 70% { + transform: translateZ(420px) rotateX(-55deg) rotateZ(5deg) translateY(-80px) + scale(1.05); + opacity: 0.8; + } + 100% { + transform: translateZ(560px) rotateX(-90deg) translateY(-160px) scale(1.2); + opacity: 0; + } } /* ─── 2. Parchment Open Book Overlay ──────── */ @@ -2405,9 +2610,9 @@ input[type=number] { background: linear-gradient(135deg, #1e0e06 0%, #2d1810 40%, #1a0d08 100%); border-radius: 6px 14px 14px 6px; box-shadow: - 0 40px 80px rgba(0,0,0,0.9), + 0 40px 80px rgba(0, 0, 0, 0.9), 0 0 0 1px #0d0705, - inset 0 1px 0 rgba(255,255,255,0.06); + inset 0 1px 0 rgba(255, 255, 255, 0.06); border: none; display: flex; padding: 18px 18px 18px 10px; @@ -2424,7 +2629,9 @@ input[type=number] { height: 100%; background: linear-gradient(90deg, #0d0705 0%, #2a1409 60%, #1a0d08 100%); border-radius: 6px 0 0 6px; - box-shadow: inset -3px 0 8px rgba(0,0,0,0.5), inset 2px 0 4px rgba(255,255,255,0.04); + box-shadow: + inset -3px 0 8px rgba(0, 0, 0, 0.5), + inset 2px 0 4px rgba(255, 255, 255, 0.04); } /* Binding stitches */ @@ -2437,8 +2644,8 @@ input[type=number] { width: 3px; background: repeating-linear-gradient( 180deg, - rgba(180,130,80,0.35) 0px, - rgba(180,130,80,0.35) 8px, + rgba(180, 130, 80, 0.35) 0px, + rgba(180, 130, 80, 0.35) 8px, transparent 8px, transparent 14px ); @@ -2448,13 +2655,19 @@ input[type=number] { /* ─── Pages Spread (Inside the Book) ─────── */ .book-pages-spread { flex: 1; - background: radial-gradient(ellipse at 30% 20%, #f8f0dd 0%, #f0e5c5 45%, #e5d4a8 80%, #d9c48e 100%); + background: radial-gradient( + ellipse at 30% 20%, + #f8f0dd 0%, + #f0e5c5 45%, + #e5d4a8 80%, + #d9c48e 100% + ); border-radius: 2px 6px 6px 2px; display: flex; position: relative; box-shadow: - inset 0 0 40px rgba(0,0,0,0.08), - 0 0 0 1px rgba(0,0,0,0.2); + inset 0 0 40px rgba(0, 0, 0, 0.08), + 0 0 0 1px rgba(0, 0, 0, 0.2); overflow: visible; } @@ -2470,9 +2683,9 @@ input[type=number] { background: linear-gradient( 90deg, transparent 0%, - rgba(0,0,0,0.18) 30%, - rgba(0,0,0,0.45) 50%, - rgba(0,0,0,0.18) 70%, + rgba(0, 0, 0, 0.18) 30%, + rgba(0, 0, 0, 0.45) 50%, + rgba(0, 0, 0, 0.18) 70%, transparent 100% ); z-index: 5; @@ -2488,7 +2701,7 @@ input[type=number] { position: relative; overflow-y: auto; overflow-x: hidden; - font-family: 'EB Garamond', serif; + font-family: "EB Garamond", serif; color: #1a1208; z-index: 2; } @@ -2496,38 +2709,137 @@ input[type=number] { /* LEFT page – torn on right edge */ .parchment-page-half.left-page { clip-path: polygon( - 0% 0%, 98% 0%, 100% 1.2%, 97% 2.5%, 99.5% 4%, 98% 5.8%, - 100% 7.5%, 97.5% 9.2%, 99% 11%, 97% 12.8%, 100% 14.5%, - 98% 16.2%, 99.5% 18%, 97% 19.8%, 100% 21.5%, 98.5% 23.2%, - 99% 25%, 97.5% 26.8%, 100% 28.5%, 98% 30.2%, 99.5% 32%, - 97% 33.8%, 100% 35.5%, 98.5% 37.2%, 99% 39%, 97.5% 40.8%, - 100% 42.5%, 98% 44.2%, 99.5% 46%, 97% 47.8%, 100% 49.5%, - 98.5% 51.2%, 99% 53%, 97.5% 54.8%, 100% 56.5%, 98% 58.2%, - 99.5% 60%, 97% 61.8%, 100% 63.5%, 98.5% 65.2%, 99% 67%, - 97.5% 68.8%, 100% 70.5%, 98% 72.2%, 99.5% 74%, 97% 75.8%, - 100% 77.5%, 98.5% 79.2%, 99% 81%, 97.5% 82.8%, 100% 84.5%, - 98% 86.2%, 99.5% 88%, 97% 89.8%, 100% 91.5%, 98.5% 93.2%, - 99% 95%, 97.5% 96.8%, 100% 98.5%, 98% 100%, 0% 100% + 0% 0%, + 98% 0%, + 100% 1.2%, + 97% 2.5%, + 99.5% 4%, + 98% 5.8%, + 100% 7.5%, + 97.5% 9.2%, + 99% 11%, + 97% 12.8%, + 100% 14.5%, + 98% 16.2%, + 99.5% 18%, + 97% 19.8%, + 100% 21.5%, + 98.5% 23.2%, + 99% 25%, + 97.5% 26.8%, + 100% 28.5%, + 98% 30.2%, + 99.5% 32%, + 97% 33.8%, + 100% 35.5%, + 98.5% 37.2%, + 99% 39%, + 97.5% 40.8%, + 100% 42.5%, + 98% 44.2%, + 99.5% 46%, + 97% 47.8%, + 100% 49.5%, + 98.5% 51.2%, + 99% 53%, + 97.5% 54.8%, + 100% 56.5%, + 98% 58.2%, + 99.5% 60%, + 97% 61.8%, + 100% 63.5%, + 98.5% 65.2%, + 99% 67%, + 97.5% 68.8%, + 100% 70.5%, + 98% 72.2%, + 99.5% 74%, + 97% 75.8%, + 100% 77.5%, + 98.5% 79.2%, + 99% 81%, + 97.5% 82.8%, + 100% 84.5%, + 98% 86.2%, + 99.5% 88%, + 97% 89.8%, + 100% 91.5%, + 98.5% 93.2%, + 99% 95%, + 97.5% 96.8%, + 100% 98.5%, + 98% 100%, + 0% 100% ); - box-shadow: inset -20px 0 30px rgba(0,0,0,0.07); + box-shadow: inset -20px 0 30px rgba(0, 0, 0, 0.07); } /* RIGHT page – torn on left edge */ .parchment-page-half.right-page { clip-path: polygon( - 0% 0%, 2% 1.2%, 0.5% 2.5%, 3% 4%, 0.5% 5.8%, 2.5% 7.5%, - 0% 9.2%, 2.5% 11%, 1% 12.8%, 3% 14.5%, 0.5% 16.2%, 2% 18%, - 0.5% 19.8%, 3% 21.5%, 0.5% 23.2%, 2% 25%, 0% 26.8%, 2.5% 28.5%, - 0.5% 30.2%, 2% 32%, 0.5% 33.8%, 3% 35.5%, 0.5% 37.2%, 2% 39%, - 0% 40.8%, 2.5% 42.5%, 0.5% 44.2%, 2% 46%, 0.5% 47.8%, 3% 49.5%, - 0.5% 51.2%, 2% 53%, 0% 54.8%, 2.5% 56.5%, 0.5% 58.2%, 2% 60%, - 0.5% 61.8%, 3% 63.5%, 0.5% 65.2%, 2% 67%, 0% 68.8%, 2.5% 70.5%, - 0.5% 72.2%, 2% 74%, 0.5% 75.8%, 3% 77.5%, 0.5% 79.2%, 2% 81%, - 0% 82.8%, 2.5% 84.5%, 0.5% 86.2%, 2% 88%, 0.5% 89.8%, 3% 91.5%, - 0.5% 93.2%, 2% 95%, 0% 96.8%, 2% 98.5%, 0.5% 100%, - 100% 100%, 100% 0% + 0% 0%, + 2% 1.2%, + 0.5% 2.5%, + 3% 4%, + 0.5% 5.8%, + 2.5% 7.5%, + 0% 9.2%, + 2.5% 11%, + 1% 12.8%, + 3% 14.5%, + 0.5% 16.2%, + 2% 18%, + 0.5% 19.8%, + 3% 21.5%, + 0.5% 23.2%, + 2% 25%, + 0% 26.8%, + 2.5% 28.5%, + 0.5% 30.2%, + 2% 32%, + 0.5% 33.8%, + 3% 35.5%, + 0.5% 37.2%, + 2% 39%, + 0% 40.8%, + 2.5% 42.5%, + 0.5% 44.2%, + 2% 46%, + 0.5% 47.8%, + 3% 49.5%, + 0.5% 51.2%, + 2% 53%, + 0% 54.8%, + 2.5% 56.5%, + 0.5% 58.2%, + 2% 60%, + 0.5% 61.8%, + 3% 63.5%, + 0.5% 65.2%, + 2% 67%, + 0% 68.8%, + 2.5% 70.5%, + 0.5% 72.2%, + 2% 74%, + 0.5% 75.8%, + 3% 77.5%, + 0.5% 79.2%, + 2% 81%, + 0% 82.8%, + 2.5% 84.5%, + 0.5% 86.2%, + 2% 88%, + 0.5% 89.8%, + 3% 91.5%, + 0.5% 93.2%, + 2% 95%, + 0% 96.8%, + 2% 98.5%, + 0.5% 100%, + 100% 100%, + 100% 0% ); - box-shadow: inset 20px 0 30px rgba(0,0,0,0.07); + box-shadow: inset 20px 0 30px rgba(0, 0, 0, 0.07); } /* Aged stain marks on pages */ @@ -2536,17 +2848,36 @@ input[type=number] { position: absolute; inset: 0; background: - radial-gradient(ellipse 60px 50px at 15% 85%, rgba(139, 90, 40, 0.06) 0%, transparent 70%), - radial-gradient(ellipse 40px 35px at 88% 12%, rgba(139, 90, 40, 0.05) 0%, transparent 70%), - radial-gradient(ellipse 80px 60px at 70% 75%, rgba(139, 90, 40, 0.04) 0%, transparent 70%); + radial-gradient( + ellipse 60px 50px at 15% 85%, + rgba(139, 90, 40, 0.06) 0%, + transparent 70% + ), + radial-gradient( + ellipse 40px 35px at 88% 12%, + rgba(139, 90, 40, 0.05) 0%, + transparent 70% + ), + radial-gradient( + ellipse 80px 60px at 70% 75%, + rgba(139, 90, 40, 0.04) 0%, + transparent 70% + ); pointer-events: none; z-index: 0; } /* Custom Scrollbar */ -.parchment-page-half::-webkit-scrollbar { width: 5px; } -.parchment-page-half::-webkit-scrollbar-track { background: rgba(0,0,0,0.04); } -.parchment-page-half::-webkit-scrollbar-thumb { background: rgba(120,80,40,0.25); border-radius: 3px; } +.parchment-page-half::-webkit-scrollbar { + width: 5px; +} +.parchment-page-half::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.04); +} +.parchment-page-half::-webkit-scrollbar-thumb { + background: rgba(120, 80, 40, 0.25); + border-radius: 3px; +} /* ─── Ink Typography ──────────────────────── */ .ink-heading { @@ -2555,10 +2886,10 @@ input[type=number] { font-style: italic; color: #0e0a04; margin-bottom: 20px; - border-bottom: 1px solid rgba(90,60,20,0.2); + border-bottom: 1px solid rgba(90, 60, 20, 0.2); padding-bottom: 12px; letter-spacing: -0.3px; - text-shadow: 0.5px 0.5px 0px rgba(0,0,0,0.08); + text-shadow: 0.5px 0.5px 0px rgba(0, 0, 0, 0.08); position: relative; z-index: 1; } @@ -2648,13 +2979,13 @@ input[type=number] { cursor: pointer; transition: all 0.3s ease; position: relative; - box-shadow: 2px 3px 8px rgba(0,0,0,0.12); + box-shadow: 2px 3px 8px rgba(0, 0, 0, 0.12); } .sketch-node-circle:hover { transform: scale(1.1) rotate(3deg); border-color: #7a1515; - box-shadow: 3px 6px 14px rgba(122,21,21,0.18); + box-shadow: 3px 6px 14px rgba(122, 21, 21, 0.18); } .sketch-node-label { @@ -2667,7 +2998,9 @@ input[type=number] { text-align: center; } -.sketch-node-circle:hover .sketch-node-label { color: #7a1515; } +.sketch-node-circle:hover .sketch-node-label { + color: #7a1515; +} .sketch-node-desc-bubble { position: absolute; @@ -2683,10 +3016,10 @@ input[type=number] { line-height: 1.4; color: #2a200e; z-index: 20; - box-shadow: 0 4px 16px rgba(0,0,0,0.18); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18); pointer-events: none; animation: tooltipFadeIn 0.2s ease forwards; - font-family: 'EB Garamond', serif; + font-family: "EB Garamond", serif; } /* ─── Contact Form (Page 4) ────── */ @@ -2699,29 +3032,40 @@ input[type=number] { z-index: 1; } -.ink-form-group { display: flex; flex-direction: column; gap: 4px; } -.ink-form-label { font-size: 13px; font-weight: 600; color: #0e0a04; letter-spacing: 0.5px; } +.ink-form-group { + display: flex; + flex-direction: column; + gap: 4px; +} +.ink-form-label { + font-size: 13px; + font-weight: 600; + color: #0e0a04; + letter-spacing: 0.5px; +} .ink-form-input { background: transparent; border: none; border-bottom: 1.5px solid #7a6040; padding: 7px 4px; - font-family: 'EB Garamond', serif; + font-family: "EB Garamond", serif; font-size: 15px; color: #0e0a04; outline: none; width: 100%; transition: border-color 0.2s ease; } -.ink-form-input:focus { border-bottom-color: #7a1515; } +.ink-form-input:focus { + border-bottom-color: #7a1515; +} .ink-form-textarea { - background: rgba(255,255,255,0.18); + background: rgba(255, 255, 255, 0.18); border: 1.5px solid #7a6040; border-radius: 3px; padding: 9px; - font-family: 'EB Garamond', serif; + font-family: "EB Garamond", serif; font-size: 15px; color: #0e0a04; outline: none; @@ -2729,7 +3073,9 @@ input[type=number] { min-height: 90px; transition: border-color 0.2s ease; } -.ink-form-textarea:focus { border-color: #7a1515; } +.ink-form-textarea:focus { + border-color: #7a1515; +} .ink-submit-button { background: linear-gradient(135deg, #7a1515 0%, #5c0f0f 100%); @@ -2737,16 +3083,18 @@ input[type=number] { border: none; border-radius: 3px; padding: 11px; - font-family: 'EB Garamond', serif; + font-family: "EB Garamond", serif; font-size: 16px; font-weight: bold; cursor: pointer; transition: all 0.2s ease; margin-top: 8px; letter-spacing: 0.5px; - box-shadow: 0 3px 10px rgba(122,21,21,0.3); + box-shadow: 0 3px 10px rgba(122, 21, 21, 0.3); +} +.ink-submit-button:hover { + background: linear-gradient(135deg, #5c0f0f 0%, #3d0a0a 100%); } -.ink-submit-button:hover { background: linear-gradient(135deg, #5c0f0f 0%, #3d0a0a 100%); } .ink-socials-grid { display: flex; @@ -2765,14 +3113,18 @@ input[type=number] { padding: 9px; border: 1px solid #7a6040; border-radius: 3px; - font-family: 'EB Garamond', serif; + font-family: "EB Garamond", serif; font-size: 14px; font-weight: 600; color: #2a200e; transition: all 0.2s ease; text-decoration: none; } -.ink-social-btn:hover { border-color: #7a1515; color: #7a1515; background: rgba(122,21,21,0.04); } +.ink-social-btn:hover { + border-color: #7a1515; + color: #7a1515; + background: rgba(122, 21, 21, 0.04); +} /* ─── Navigation Buttons ───────────────────── */ .book-close-x { @@ -2807,7 +3159,7 @@ input[type=number] { gap: 7px; background: transparent; border: none; - font-family: 'EB Garamond', serif; + font-family: "EB Garamond", serif; font-size: 15px; font-weight: bold; color: #7a1515; @@ -2816,19 +3168,27 @@ input[type=number] { letter-spacing: 0.3px; z-index: 10; } -.book-page-turn-btn.prev-btn { left: 44px; } -.book-page-turn-btn.prev-btn:hover { transform: translateX(-4px); } -.book-page-turn-btn.next-btn { right: 44px; } -.book-page-turn-btn.next-btn:hover { transform: translateX(4px); } +.book-page-turn-btn.prev-btn { + left: 44px; +} +.book-page-turn-btn.prev-btn:hover { + transform: translateX(-4px); +} +.book-page-turn-btn.next-btn { + right: 44px; +} +.book-page-turn-btn.next-btn:hover { + transform: translateX(4px); +} .book-page-indicator { position: absolute; bottom: 26px; left: 50%; transform: translateX(-50%); - font-family: 'EB Garamond', serif; + font-family: "EB Garamond", serif; font-size: 13px; - color: rgba(42,32,14,0.5); + color: rgba(42, 32, 14, 0.5); letter-spacing: 2px; z-index: 10; pointer-events: none; @@ -2873,7 +3233,7 @@ input[type=number] { /* Subtle animated neon sweep on hover */ .lock-strategy-btn::before { - content: ''; + content: ""; position: absolute; top: 0; left: -75%; @@ -2895,18 +3255,20 @@ input[type=number] { } /* Target / Stop Loss number inputs inside the lock-strategy-panel */ -.lock-strategy-panel input[type='number'] { - transition: border-color 0.2s ease, box-shadow 0.2s ease; +.lock-strategy-panel input[type="number"] { + transition: + border-color 0.2s ease, + box-shadow 0.2s ease; } -.lock-strategy-panel input[type='number']:focus { +.lock-strategy-panel input[type="number"]:focus { border-color: var(--neon-cyan); box-shadow: 0 0 0 2px rgba(0, 242, 254, 0.15); } /* Hide native number spinner arrows for a cleaner look (optional) */ -.lock-strategy-panel input[type='number']::-webkit-outer-spin-button, -.lock-strategy-panel input[type='number']::-webkit-inner-spin-button { +.lock-strategy-panel input[type="number"]::-webkit-outer-spin-button, +.lock-strategy-panel input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } @@ -2940,26 +3302,53 @@ input[type=number] { } } - /* ─── Animations ──────────────────────────── */ @keyframes backdropFadeIn { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } @keyframes bookReaderAppear { - from { opacity: 0; transform: scale(0.88) translateY(30px) rotate(-2deg); } - to { opacity: 1; transform: scale(1) translateY(0) rotate(0deg); } + from { + opacity: 0; + transform: scale(0.88) translateY(30px) rotate(-2deg); + } + to { + opacity: 1; + transform: scale(1) translateY(0) rotate(0deg); + } } @keyframes bookFlyOpen { - 0% { transform: translateY(0) rotate(0deg) scale(1); opacity: 1; } - 40% { transform: translateY(-90px) rotate(-8deg) scale(1.06); opacity: 1; } - 70% { transform: translateY(-120px) rotate(4deg) scale(1.1); opacity: 0.85; } - 100% { transform: translateY(-200px) rotate(0deg) scale(1.2); opacity: 0; } + 0% { + transform: translateY(0) rotate(0deg) scale(1); + opacity: 1; + } + 40% { + transform: translateY(-90px) rotate(-8deg) scale(1.06); + opacity: 1; + } + 70% { + transform: translateY(-120px) rotate(4deg) scale(1.1); + opacity: 0.85; + } + 100% { + transform: translateY(-200px) rotate(0deg) scale(1.2); + opacity: 0; + } } @keyframes tooltipFadeIn { - from { opacity: 0; transform: translate(-50%, 6px); } - to { opacity: 1; transform: translate(-50%, 0); } + from { + opacity: 0; + transform: translate(-50%, 6px); + } + to { + opacity: 1; + transform: translate(-50%, 0); + } } diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index 4878f24..c7b445b 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -1,12 +1,22 @@ -import { useState, useEffect, useRef } from 'react'; -import { Button } from '../components/ui/button'; -import { X, Eye, EyeOff, Lock, Mail, User, Globe } from 'lucide-react'; - -import Logo from '../components/Logo'; -import LogoLoop from '../components/LogoLoop'; -import Sparkline from '../components/Sparkline'; -import { SiReact, SiTypescript, SiFastapi, SiPostgresql, SiGraphql, SiApachekafka, SiRazorpay, SiTailwindcss, SiPython, SiGrafana } from 'react-icons/si'; - +import { useState, useEffect, useRef } from "react"; +import { Button } from "../components/ui/button"; +import { X, Eye, EyeOff, Lock, Mail, User, Globe } from "lucide-react"; + +import Logo from "../components/Logo"; +import LogoLoop from "../components/LogoLoop"; +import Sparkline from "../components/Sparkline"; +import { + SiReact, + SiTypescript, + SiFastapi, + SiPostgresql, + SiGraphql, + SiApachekafka, + SiRazorpay, + SiTailwindcss, + SiPython, + SiGrafana, +} from "react-icons/si"; interface LandingPageProps { onGoogleLogin: (response: any) => Promise; @@ -17,7 +27,7 @@ interface LandingPageProps { onGoToDashboard?: () => void; } -type AuthMode = 'signin' | 'signup' | 'verify'; +type AuthMode = "signin" | "signup" | "verify"; function GithubIcon({ size = 14, className = "" }) { return ( @@ -39,29 +49,35 @@ function GithubIcon({ size = 14, className = "" }) { ); } -export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSuccess, apiUrl, user, onGoToDashboard }: LandingPageProps) { +export default function LandingPage({ + onGoogleLogin, + googleClientId, + onAuthSuccess, + apiUrl, + user, + onGoToDashboard, +}: LandingPageProps) { const [showAuthModal, setShowAuthModal] = useState(false); - const [showReachUs, setShowReachUs] = useState(false); - const [authMode, setAuthMode] = useState('signin'); - + const [authMode, setAuthMode] = useState("signin"); + // Contact Form States const [showContactModal, setShowContactModal] = useState(false); - const [contactName, setContactName] = useState(''); - const [contactEmail, setContactEmail] = useState(''); - const [contactMessage, setContactMessage] = useState(''); + const [contactName, setContactName] = useState(""); + const [contactEmail, setContactEmail] = useState(""); + const [contactMessage, setContactMessage] = useState(""); const [contactLoading, setContactLoading] = useState(false); const [contactError, setContactError] = useState(null); const [contactSuccess, setContactSuccess] = useState(null); - + // Form States - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [fullName, setFullName] = useState(''); - const [country, setCountry] = useState(''); - + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [fullName, setFullName] = useState(""); + const [country, setCountry] = useState(""); + // UI Helpers const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); @@ -69,24 +85,24 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce const [errorMsg, setErrorMsg] = useState(null); const [successMsg, setSuccessMsg] = useState(null); const [githubStars, setGithubStars] = useState(null); - + // OTP Verification States - const [otp, setOtp] = useState(''); - const [emailToVerify, setEmailToVerify] = useState(''); + const [otp, setOtp] = useState(""); + const [emailToVerify, setEmailToVerify] = useState(""); const [resendCooldown, setResendCooldown] = useState(0); // OTP Countdown timer useEffect(() => { if (resendCooldown <= 0) return; - const timer = setTimeout(() => setResendCooldown(prev => prev - 1), 1000); + const timer = setTimeout(() => setResendCooldown((prev) => prev - 1), 1000); return () => clearTimeout(timer); }, [resendCooldown]); // Reset verification states when auth modal closes useEffect(() => { if (!showAuthModal) { - setOtp(''); - setEmailToVerify(''); + setOtp(""); + setEmailToVerify(""); setResendCooldown(0); setErrorMsg(null); setSuccessMsg(null); @@ -97,14 +113,14 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce // Fetch actual GitHub stargazers count for the repository useEffect(() => { - fetch('https://api.github.com/repos/Edge-Explorer/QuantIQ') - .then(res => { + fetch("https://api.github.com/repos/Edge-Explorer/QuantIQ") + .then((res) => { if (!res.ok) throw new Error(); return res.json(); }) - .then(data => { + .then((data) => { const count = data.stargazers_count; - if (typeof count === 'number') { + if (typeof count === "number") { if (count >= 1000) { setGithubStars(`${(count / 1000).toFixed(1)}K`); } else { @@ -114,34 +130,106 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce }) .catch(() => { // Fallback default - setGithubStars('Star'); + setGithubStars("Star"); }); }, []); // Market Movers live state const [marketMovers, setMarketMovers] = useState<{ - gainers: any[]; losers: any[]; most_active: any[]; + gainers: any[]; + losers: any[]; + most_active: any[]; }>({ gainers: [ - { symbol: 'SLBT', name: 'SL Science Holding', price: 5.99, changePercent: 34.61 }, - { symbol: 'PLBL', name: 'Polibeli Group Ltd', price: 10.26, changePercent: 18.20 }, - { symbol: 'GPC', name: 'Genuine Parts Co.', price: 132.57, changePercent: 12.92 }, - { symbol: 'SLS', name: 'SELLAS Life Sciences', price: 14.98, changePercent: 12.89 }, - { symbol: 'CAR', name: 'Avis Budget Group', price: 163.44, changePercent: 11.23 }, + { + symbol: "SLBT", + name: "SL Science Holding", + price: 5.99, + changePercent: 34.61, + }, + { + symbol: "PLBL", + name: "Polibeli Group Ltd", + price: 10.26, + changePercent: 18.2, + }, + { + symbol: "GPC", + name: "Genuine Parts Co.", + price: 132.57, + changePercent: 12.92, + }, + { + symbol: "SLS", + name: "SELLAS Life Sciences", + price: 14.98, + changePercent: 12.89, + }, + { + symbol: "CAR", + name: "Avis Budget Group", + price: 163.44, + changePercent: 11.23, + }, ], losers: [ - { symbol: 'RGC', name: 'Regencell Bioscience', price: 6.37, changePercent: -20.67 }, - { symbol: 'VICR', name: 'Vicor Corporation', price: 282.95, changePercent: -19.21 }, - { symbol: 'ACLS', name: 'Axcelis Technologies', price: 144.50, changePercent: -18.97 }, - { symbol: 'VECO', name: 'Veeco Instruments', price: 57.49, changePercent: -18.48 }, - { symbol: 'BELFA', name: 'Bel Fuse Inc.', price: 230.16, changePercent: -18.29 }, + { + symbol: "RGC", + name: "Regencell Bioscience", + price: 6.37, + changePercent: -20.67, + }, + { + symbol: "VICR", + name: "Vicor Corporation", + price: 282.95, + changePercent: -19.21, + }, + { + symbol: "ACLS", + name: "Axcelis Technologies", + price: 144.5, + changePercent: -18.97, + }, + { + symbol: "VECO", + name: "Veeco Instruments", + price: 57.49, + changePercent: -18.48, + }, + { + symbol: "BELFA", + name: "Bel Fuse Inc.", + price: 230.16, + changePercent: -18.29, + }, ], most_active: [ - { symbol: 'AAL', name: 'American Airlines', price: 17.92, changePercent: -1.27 }, - { symbol: 'T', name: 'AT&T Inc.', price: 20.58, changePercent: 0.49 }, - { symbol: 'NVDA', name: 'NVIDIA Corporation', price: 194.83, changePercent: -1.39 }, - { symbol: 'INTC', name: 'Intel Corporation', price: 120.35, changePercent: -5.25 }, - { symbol: 'OPEN', name: 'Opendoor Technologies', price: 4.90, changePercent: -0.81 }, + { + symbol: "AAL", + name: "American Airlines", + price: 17.92, + changePercent: -1.27, + }, + { symbol: "T", name: "AT&T Inc.", price: 20.58, changePercent: 0.49 }, + { + symbol: "NVDA", + name: "NVIDIA Corporation", + price: 194.83, + changePercent: -1.39, + }, + { + symbol: "INTC", + name: "Intel Corporation", + price: 120.35, + changePercent: -5.25, + }, + { + symbol: "OPEN", + name: "Opendoor Technologies", + price: 4.9, + changePercent: -0.81, + }, ], }); @@ -155,69 +243,81 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce if (active && data.gainers?.length) setMarketMovers(data); } } catch (err) { - console.error('Landing market movers fetch failed:', err); + console.error("Landing market movers fetch failed:", err); } }; fetchMovers(); const interval = setInterval(fetchMovers, 60000); - return () => { active = false; clearInterval(interval); }; + return () => { + active = false; + clearInterval(interval); + }; }, [apiUrl]); // Landing Page News state const [landingNews, setLandingNews] = useState([ { id: 1, - title: 'Federal Reserve Signals Potential Rate Cuts Later This Year', - summary: 'Fed Chair Powell indicated inflation is returning to the 2% target path, hinting at upcoming rate adjustments that could fuel market momentum.', - source: 'Bloomberg', - time: '12m ago', - category: 'Macro', - link: 'https://www.bloomberg.com/markets' + title: "Federal Reserve Signals Potential Rate Cuts Later This Year", + summary: + "Fed Chair Powell indicated inflation is returning to the 2% target path, hinting at upcoming rate adjustments that could fuel market momentum.", + source: "Bloomberg", + time: "12m ago", + category: "Macro", + link: "https://www.bloomberg.com/markets", }, { id: 2, - title: 'NVIDIA Demand Outstrips Supply as Tech Giants Expand AI Infrastructure', - summary: 'Top cloud providers continue to place record-breaking chip orders. Financial firms hike price targets as AI hardware revenues reach unprecedented highs.', - source: 'Reuters', - time: '35m ago', - category: 'Technology', - link: 'https://www.reuters.com/technology' + title: + "NVIDIA Demand Outstrips Supply as Tech Giants Expand AI Infrastructure", + summary: + "Top cloud providers continue to place record-breaking chip orders. Financial firms hike price targets as AI hardware revenues reach unprecedented highs.", + source: "Reuters", + time: "35m ago", + category: "Technology", + link: "https://www.reuters.com/technology", }, { id: 3, - title: 'Bitcoin Solidifies Base Around $60K; On-Chain Accumulation Spikes', - summary: 'Market intelligence data shows heavy whale wallet accumulation at current support levels, indicating solid long-term investor conviction.', - source: 'CoinDesk', - time: '1h ago', - category: 'Crypto', - link: 'https://www.coindesk.com' + title: + "Bitcoin Solidifies Base Around $60K; On-Chain Accumulation Spikes", + summary: + "Market intelligence data shows heavy whale wallet accumulation at current support levels, indicating solid long-term investor conviction.", + source: "CoinDesk", + time: "1h ago", + category: "Crypto", + link: "https://www.coindesk.com", }, { id: 4, - title: 'Global Tech Stock Indexes Experience Rotational Capital Inflows', - summary: 'Defensive sector gains support stock index benchmarks as fund managers rebalance portfolios ahead of upcoming CPI updates.', - source: 'CNBC', - time: '2h ago', - category: 'Markets', - link: 'https://www.cnbc.com/markets' + title: "Global Tech Stock Indexes Experience Rotational Capital Inflows", + summary: + "Defensive sector gains support stock index benchmarks as fund managers rebalance portfolios ahead of upcoming CPI updates.", + source: "CNBC", + time: "2h ago", + category: "Markets", + link: "https://www.cnbc.com/markets", }, { id: 5, - title: 'Tesla Q3 Deliveries Beat Expectations; Stock Surges Pre-Market', - summary: 'Tesla reported record deliveries surpassing analyst forecasts, signaling strong demand recovery and boosting EV market confidence.', - source: 'MarketWatch', - time: '3h ago', - category: 'Stocks', - link: 'https://www.marketwatch.com' + title: "Tesla Q3 Deliveries Beat Expectations; Stock Surges Pre-Market", + summary: + "Tesla reported record deliveries surpassing analyst forecasts, signaling strong demand recovery and boosting EV market confidence.", + source: "MarketWatch", + time: "3h ago", + category: "Stocks", + link: "https://www.marketwatch.com", }, { id: 6, - title: 'Apple Vision Pro Drives New Wave of Spatial Computing Investments', - summary: 'Institutional investors are increasing exposure to companies developing spatial computing platforms following Apple\'s growing developer ecosystem.', - source: 'Wall Street Journal', - time: '4h ago', - category: 'Technology', - link: 'https://www.wsj.com/tech' + title: + "Apple Vision Pro Drives New Wave of Spatial Computing Investments", + summary: + "Institutional investors are increasing exposure to companies developing spatial computing platforms following Apple's growing developer ecosystem.", + source: "Wall Street Journal", + time: "4h ago", + category: "Technology", + link: "https://www.wsj.com/tech", }, ]); @@ -229,30 +329,35 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce if (res.ok) { const data = await res.json(); if (data && data.length > 0 && active) { - setLandingNews(data.map((item: any) => ({ - id: item.id || Math.random().toString(), - title: item.title, - summary: item.summary || '', - source: item.source || 'Finance', - time: item.time || 'Recent', - category: item.category || 'Markets', - link: item.link || item.url || item.article_url || null, - }))); + setLandingNews( + data.map((item: any) => ({ + id: item.id || Math.random().toString(), + title: item.title, + summary: item.summary || "", + source: item.source || "Finance", + time: item.time || "Recent", + category: item.category || "Markets", + link: item.link || item.url || item.article_url || null, + })), + ); } } } catch (err) { - console.error('Landing news fetch failed:', err); + console.error("Landing news fetch failed:", err); } }; fetchLandingNews(); const newsInterval = setInterval(fetchLandingNews, 300000); // refresh every 5 min - return () => { active = false; clearInterval(newsInterval); }; + return () => { + active = false; + clearInterval(newsInterval); + }; }, [apiUrl]); useEffect(() => { // Append Google Identity Services SDK script - const script = document.createElement('script'); - script.src = 'https://accounts.google.com/gsi/client'; + const script = document.createElement("script"); + script.src = "https://accounts.google.com/gsi/client"; script.async = true; script.defer = true; document.body.appendChild(script); @@ -261,12 +366,13 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce if (window.google && googleButtonRef.current) { try { window.google.accounts.id.initialize({ - client_id: googleClientId || 'dummy-client-id.apps.googleusercontent.com', + client_id: + googleClientId || "dummy-client-id.apps.googleusercontent.com", callback: onGoogleLogin, }); window.google.accounts.id.renderButton(googleButtonRef.current, { - theme: 'filled_blue', - size: 'large', + theme: "filled_blue", + size: "large", width: 280, }); } catch (err) { @@ -288,8 +394,8 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce useEffect(() => { if (showAuthModal && window.google && googleButtonRef.current) { window.google.accounts.id.renderButton(googleButtonRef.current, { - theme: 'filled_blue', - size: 'large', + theme: "filled_blue", + size: "large", width: 280, }); } @@ -301,17 +407,21 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce const toggleAuthMode = () => { setErrorMsg(null); setSuccessMsg(null); - if (authMode === 'verify') { - setAuthMode('signin'); + if (authMode === "verify") { + setAuthMode("signin"); } else { - setAuthMode(prev => prev === 'signin' ? 'signup' : 'signin'); + setAuthMode((prev) => (prev === "signin" ? "signup" : "signin")); } }; // Fetch with automatic retry on 503/5xx (HF Space load-balancer multi-replica issue) // HF free tier has 2 replicas — one may be dead. We retry up to 6 times with random // jitter so each attempt independently re-rolls the load balancer lottery. - const fetchWithRetry = async (url: string, options: RequestInit, retries = 6): Promise => { + const fetchWithRetry = async ( + url: string, + options: RequestInit, + retries = 6, + ): Promise => { for (let attempt = 1; attempt <= retries; attempt++) { const response = await fetch(url, options); // If 5xx and not last attempt, wait and retry @@ -319,7 +429,7 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce // Random jitter 800ms–1600ms so each retry independently hits the LB const jitter = 800 + Math.random() * 800; setErrorMsg(`Connecting to server… retrying (${attempt}/${retries})`); - await new Promise(res => setTimeout(res, jitter)); + await new Promise((res) => setTimeout(res, jitter)); continue; } return response; @@ -339,7 +449,7 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce return; } - if (authMode === 'signup') { + if (authMode === "signup") { if (!fullName || !country) { setErrorMsg("All fields are required for sign up."); return; @@ -357,22 +467,24 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce setLoading(true); try { - if (authMode === 'signup') { + if (authMode === "signup") { const response = await fetchWithRetry(`${apiUrl}/api/v1/auth/signup`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim(), full_name: fullName.trim(), country: country.trim(), - password: password - }) + password: password, + }), }); // Guard against non-JSON 503 HTML responses from HF - const contentType = response.headers.get('content-type') || ''; - if (!contentType.includes('application/json')) { - throw new Error("The server is temporarily unavailable. Please try again in a few seconds."); + const contentType = response.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + throw new Error( + "The server is temporarily unavailable. Please try again in a few seconds.", + ); } const data = await response.json(); @@ -382,12 +494,14 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce if (data.verification_required) { setEmailToVerify(email.trim()); - setAuthMode('verify'); - setOtp(''); + setAuthMode("verify"); + setOtp(""); setResendCooldown(60); - setSuccessMsg("Account registered! A 6-digit verification code has been sent to your email."); - setPassword(''); - setConfirmPassword(''); + setSuccessMsg( + "Account registered! A 6-digit verification code has been sent to your email.", + ); + setPassword(""); + setConfirmPassword(""); } else if (data.access_token) { setSuccessMsg("Account created successfully!"); setTimeout(() => { @@ -398,18 +512,20 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce } else { // Sign In — with retry on 503 const response = await fetchWithRetry(`${apiUrl}/api/v1/auth/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim(), - password: password - }) + password: password, + }), }); // Guard against non-JSON 503 HTML responses from HF - const contentType = response.headers.get('content-type') || ''; - if (!contentType.includes('application/json')) { - throw new Error("The server is temporarily unavailable. Please wait a moment and try again."); + const contentType = response.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + throw new Error( + "The server is temporarily unavailable. Please wait a moment and try again.", + ); } const data = await response.json(); @@ -419,11 +535,13 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce if (data.verification_required) { setEmailToVerify(email.trim()); - setAuthMode('verify'); - setOtp(''); + setAuthMode("verify"); + setOtp(""); setResendCooldown(60); - setSuccessMsg("Your account is not verified yet. A new verification code has been sent to your email."); - setPassword(''); + setSuccessMsg( + "Your account is not verified yet. A new verification code has been sent to your email.", + ); + setPassword(""); } else if (data.access_token) { setErrorMsg(null); onAuthSuccess(data.access_token); @@ -451,17 +569,19 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce try { const response = await fetchWithRetry(`${apiUrl}/api/v1/auth/verify`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: emailToVerify, - code: otp.trim() - }) + code: otp.trim(), + }), }); - const contentType = response.headers.get('content-type') || ''; - if (!contentType.includes('application/json')) { - throw new Error("The server is temporarily unavailable. Please try again in a few seconds."); + const contentType = response.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + throw new Error( + "The server is temporarily unavailable. Please try again in a few seconds.", + ); } const data = await response.json(); @@ -489,17 +609,22 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce setSuccessMsg(null); try { - const response = await fetchWithRetry(`${apiUrl}/api/v1/auth/resend-code`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email: emailToVerify - }) - }); - - const contentType = response.headers.get('content-type') || ''; - if (!contentType.includes('application/json')) { - throw new Error("The server is temporarily unavailable. Please try again in a few seconds."); + const response = await fetchWithRetry( + `${apiUrl}/api/v1/auth/resend-code`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: emailToVerify, + }), + }, + ); + + const contentType = response.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + throw new Error( + "The server is temporarily unavailable. Please try again in a few seconds.", + ); } const data = await response.json(); @@ -528,8 +653,8 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce try { const response = await fetch(`${apiUrl}/api/v1/contact`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: contactName.trim(), email: contactEmail.trim(), @@ -543,9 +668,9 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce } setContactSuccess("Message sent successfully!"); - setContactName(''); - setContactEmail(''); - setContactMessage(''); + setContactName(""); + setContactEmail(""); + setContactMessage(""); } catch (err: any) { setContactError(err.message || "An unexpected error occurred."); } finally { @@ -554,10 +679,15 @@ export default function LandingPage({ onGoogleLogin, googleClientId, onAuthSucce }; return ( -
- +
{/* 1. Fullscreen Fixed Dark Fallback + Background Video */} -
+