diff --git a/.gitignore b/.gitignore index 724549c..68a9599 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.key *.env *secrets.js +secrets.js trahn-trade-frontend/src/auth/secrets.js* # ============================================ diff --git a/trahn-trade-frontend/index.html b/trahn-trade-frontend/index.html index 8bf5084..79b13ce 100644 --- a/trahn-trade-frontend/index.html +++ b/trahn-trade-frontend/index.html @@ -5,8 +5,6 @@ TRAHN Grid Trader - -
diff --git a/trahn-trade-frontend/src/App.css b/trahn-trade-frontend/src/App.css index 3fff0d5..7b6075e 100644 --- a/trahn-trade-frontend/src/App.css +++ b/trahn-trade-frontend/src/App.css @@ -1,612 +1,649 @@ :root { - --bg-primary: #0d1117; - --bg-secondary: #161b22; - --bg-tertiary: #21262d; - --text-primary: #e6edf3; - --text-secondary: #8b949e; - --text-muted: #484f58; - --accent: #58a6ff; - --green: #3fb950; - --green-fill: rgba(63, 185, 80, 0.15); - --red: #f85149; - --red-fill: rgba(248, 81, 73, 0.15); - --yellow: #f0c000; - --orange: #db6d28; - --border: #30363d; + --bg-primary: #000000; + --bg-secondary: #070808; + --bg-tertiary: #363737; + --text-primary: #01ff3c; + --text-secondary: #01ff3c; + --text-muted: #484f58; + --accent: #58a6ff; + --green: #3fb950; + --green-fill: rgba(63, 185, 80, 0.15); + --red: #f85149; + --red-fill: rgba(248, 81, 73, 0.15); + --yellow: #f0c000; + --orange: #db6d28; + --border: #30363d; } * { - margin: 0; - padding: 0; - box-sizing: border-box; + margin: 0; + padding: 0; + box-sizing: border-box; } body { - font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif; - background: var(--bg-primary); - color: var(--text-primary); - min-height: 100vh; - padding: 2rem; + font-family: 'Ac437_Rainbow100_re_132', monospace; + font-size: 18px; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + padding: 2rem; } .container { - max-width: 1200px; - margin: 0 auto; + max-width: 1200px; + margin: 0 auto; } header { - text-align: center; - margin-bottom: 2rem; + display: flex; + flex-direction: column; + text-align: center; + margin-right: auto; + margin-left: auto; + margin-bottom: 2rem; } -.ascii-logo { - font-family: 'JetBrains Mono', monospace; - font-size: 0.65rem; - line-height: 1.1; - color: var(--accent); - margin: 0; - display: inline-block; +.logo { + margin-left: 32px; + font-family: 'Ac437_Rainbow100_re_132', monospace; + font-size: 9rem; + font-weight: 700; + color: var(--text-primary); + text-align: center; + letter-spacing: 0.25em; + text-shadow: 0 10 30px rgba(88, 166, 255, 0.3); } .subtitle { - font-family: 'JetBrains Mono', monospace; - font-size: 0.875rem; - color: var(--text-secondary); - letter-spacing: 0.05em; + font-family: 'Ac437_Rainbow100_re_132', monospace; + font-size: 0.875rem; + color: var(--text-secondary); + letter-spacing: 0.05em; } /* Carousel Navigation */ .carousel-nav { - display: flex; - align-items: center; - justify-content: center; - gap: 1.5rem; - margin-bottom: 1rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; + margin-bottom: 1rem; } .nav-arrow { - background: var(--bg-secondary); - border: 1px solid var(--border); - color: var(--text-primary); - width: 40px; - height: 40px; - border-radius: 8px; - cursor: pointer; - font-size: 1rem; - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; + background: var(--bg-secondary); + border: 1px solid var(--border); + color: var(--text-primary); + width: 40px; + height: 40px; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; } .nav-arrow:hover:not(:disabled) { - background: var(--bg-tertiary); - border-color: var(--accent); - color: var(--accent); + background: var(--bg-tertiary); + border-color: var(--accent); + color: var(--accent); } .nav-arrow:disabled { - opacity: 0.3; - cursor: not-allowed; + opacity: 0.3; + cursor: not-allowed; } .date-display { - text-align: center; - min-width: 200px; + text-align: center; + min-width: 200px; } .date-label { - display: block; - font-size: 1.25rem; - font-weight: 600; - color: var(--text-primary); + font-family: 'Ac437_Rainbow100_re_132'; + font-weight: 600; + font-size: 1.45rem; + font-weight: 600; + color: var(--text-primary); } .date-status { - display: block; - font-family: 'JetBrains Mono', monospace; - font-size: 0.75rem; - color: var(--text-secondary); - margin-top: 0.25rem; + display: block; + font-family: 'Ac437_Rainbow100_re_132', monospace; + font-size: 0.75rem; + color: var(--text-secondary); + margin-top: 0.25rem; } .date-status.live { - color: var(--green); + color: var(--green); } .date-status.live::before { - content: '●'; - margin-right: 0.5rem; - animation: pulse 1.5s ease-in-out infinite; + content: '●'; + margin-right: 0.5rem; + animation: pulse 1.5s ease-in-out infinite; } @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.4; + } } /* Chart */ .chart-wrapper { - position: relative; - margin-bottom: 1.5rem; + position: relative; + margin-bottom: 1.5rem; } .chart-container { - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 12px; - padding: 1.5rem; - height: 400px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.5rem; + height: 400px; } .tooltip { - position: absolute; - background: var(--bg-tertiary); - border: 1px solid var(--border); - border-radius: 6px; - padding: 0.5rem 0.75rem; - font-family: 'JetBrains Mono', monospace; - font-size: 0.75rem; - pointer-events: none; - opacity: 0; - transition: opacity 0.15s ease; - z-index: 100; - white-space: nowrap; + position: absolute; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.5rem 0.75rem; + font-family: 'Ac437_Rainbow100_re_132', monospace; + font-size: 0.75rem; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; + z-index: 100; + white-space: nowrap; } .tooltip.visible { - opacity: 1; + opacity: 1; } .tooltip .price { - font-size: 0.875rem; - font-weight: 600; - color: var(--text-primary); + font-size: 0.875rem; + font-weight: 600; + color: var(--text-primary); } .tooltip .time { - color: var(--text-secondary); - margin-top: 2px; + color: var(--text-secondary); + margin-top: 2px; } /* Loading overlay */ .loading-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--bg-secondary); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; - border-radius: 12px; - color: var(--text-secondary); - font-family: 'JetBrains Mono', monospace; - z-index: 50; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--bg-secondary); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + border-radius: 12px; + color: var(--text-secondary); + font-family: 'Ac437_Rainbow100_re_132', monospace; + z-index: 50; } .spinner { - width: 30px; - height: 30px; - border: 3px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 1s linear infinite; + width: 30px; + height: 30px; + border: 3px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 1s linear infinite; } @keyframes spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } /* Stats */ .stats { - display: flex; - justify-content: space-between; - gap: 1rem; - margin-bottom: 1.5rem; + display: flex; + font-size: 1.2rem; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; } .stat { - flex: 1; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 8px; - padding: 1rem; - text-align: center; + flex: 1; + font-size: 1.2rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + text-align: center; } .stat-label { - display: block; - font-size: 0.75rem; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 0.25rem; + display: block; + font-size: 0.75rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.25rem; } .stat-value { - font-family: 'JetBrains Mono', monospace; - font-size: 1.25rem; - font-weight: 600; + font-family: 'Ac437_Rainbow100_re_132', monospace; + font-size: 1.25rem; + font-weight: 600; } -.stat-value.high { color: var(--green); } -.stat-value.low { color: var(--red); } -.stat-value.buy { color: var(--yellow); } -.stat-value.sell { color: var(--orange); } +.stat-value.high { + color: var(--green); +} + +.stat-value.low { + color: var(--red); +} + +.stat-value.buy { + color: var(--yellow); +} + +.stat-value.sell { + color: var(--orange); +} /* Day indicators */ .day-indicators { - display: flex; - justify-content: center; - gap: 0.5rem; - margin-bottom: 1rem; + display: flex; + justify-content: center; + gap: 0.5rem; + margin-bottom: 1rem; } .day-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--border); - cursor: pointer; - transition: all 0.2s ease; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--border); + cursor: pointer; + transition: all 0.2s ease; } .day-dot:hover { - background: var(--text-secondary); + background: var(--text-secondary); } .day-dot.active { - background: var(--accent); - box-shadow: 0 0 8px var(--accent); + background: var(--accent); + box-shadow: 0 0 8px var(--accent); } /* Legend */ .legend { - display: flex; - justify-content: center; - gap: 2rem; - font-size: 0.875rem; - color: var(--text-secondary); + display: flex; + justify-content: center; + gap: 2rem; + font-size: 0.875rem; + color: var(--text-secondary); } .legend-item { - display: flex; - align-items: center; - gap: 0.5rem; + display: flex; + align-items: center; + font-size: 1rem; + gap: 0.5rem; } .dot { - width: 10px; - height: 10px; - border-radius: 50%; + width: 10px; + height: 10px; + border-radius: 50%; } .buy-dot { - background: var(--yellow); - box-shadow: 0 0 8px var(--yellow); + background: var(--yellow); + box-shadow: 0 0 8px var(--yellow); } .sell-dot { - background: var(--orange); - box-shadow: 0 0 8px var(--orange); + background: var(--orange); + box-shadow: 0 0 8px var(--orange); } /* Connection status */ .connection-status { - position: fixed; - bottom: 1rem; - right: 1rem; - display: flex; - align-items: center; - gap: 0.5rem; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 6px; - padding: 0.5rem 0.75rem; - font-family: 'JetBrains Mono', monospace; - font-size: 0.75rem; - color: var(--text-secondary); + position: fixed; + bottom: 1rem; + right: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.5rem 0.75rem; + font-family: 'Ac437_Rainbow100_re_132', monospace; + font-size: 0.75rem; + color: var(--text-secondary); } .connection-status .status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--text-muted); + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-muted); } .connection-status.connected .status-dot { - background: var(--green); - box-shadow: 0 0 6px var(--green); + background: var(--green); + box-shadow: 0 0 6px var(--green); } .connection-status.waiting .status-dot { - background: var(--yellow); - animation: pulse 1.5s ease-in-out infinite; + background: var(--yellow); + animation: pulse 1.5s ease-in-out infinite; } /* Responsive */ @media (max-width: 768px) { - body { - padding: 1rem; - } - - h1 { - font-size: 1.75rem; - } - - .stats { - flex-wrap: wrap; - } - - .stat { - flex: 1 1 45%; - } - - .chart-container { - height: 300px; - } + body { + padding: 1rem; + } + + h1 { + font-size: 1.75rem; + } + + .stats { + flex-wrap: wrap; + } + + .stat { + flex: 1 1 45%; + } + + .chart-container { + height: 300px; + } } /* Grid Levels Display */ .grid-levels { - margin-top: 1.5rem; - padding: 1rem; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 8px; + margin-top: 1.5rem; + padding: 1rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; } .grid-levels-header { - text-align: center; - font-size: 0.875rem; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: 1rem; - text-transform: uppercase; - letter-spacing: 0.05em; + text-align: center; + font-size: 1.2rem; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 1rem; + text-transform: uppercase; + letter-spacing: 0.05em; } .grid-levels-container { - display: flex; - justify-content: center; - gap: 2rem; + display: flex; + justify-content: center; + gap: 2rem; } .grid-column { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.375rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.375rem; } .grid-column-header { - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 0.5rem; - padding: 0.25rem 0.75rem; - border-radius: 4px; + font-size: 1.2rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.5rem; + padding: 0.25rem 0.75rem; + border-radius: 4px; } .grid-column-header.buy { - color: var(--yellow); - background: rgba(240, 192, 0, 0.1); + font-size: 1.2rem; + color: var(--yellow); + background: rgba(240, 192, 0, 0.1); } .grid-column-header.sell { - color: var(--orange); - background: rgba(219, 109, 40, 0.1); + font-size: 1.2rem; + color: var(--orange); + background: rgba(219, 109, 40, 0.1); } .grid-level { - font-family: 'JetBrains Mono', monospace; - font-size: 0.8rem; - padding: 0.25rem 0.75rem; - border-radius: 4px; - min-width: 100px; - text-align: center; + font-family: 'Ac437_Rainbow100_re_132', monospace; + font-size: 1.2rem; + padding: 0.25rem 0.75rem; + border-radius: 4px; + min-width: 100px; + text-align: center; } .grid-level.buy { - color: var(--yellow); - background: rgba(240, 192, 0, 0.08); - border: 1px solid rgba(240, 192, 0, 0.2); + color: var(--yellow); + font-size: 1.2rem; + background: rgba(240, 192, 0, 0.08); + border: 1px solid rgba(240, 192, 0, 0.2); } .grid-level.sell { - color: var(--orange); - background: rgba(219, 109, 40, 0.08); - border: 1px solid rgba(219, 109, 40, 0.2); + color: var(--orange); + background: rgba(219, 109, 40, 0.08); + border: 1px solid rgba(219, 109, 40, 0.2); } .grid-level.filled { - opacity: 0.5; - text-decoration: line-through; + opacity: 0.5; + text-decoration: line-through; } /* Auth Pages */ .auth-container { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - padding: 2rem; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; } .auth-card { - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 12px; - padding: 2.5rem; - width: 100%; - max-width: 420px; - text-align: center; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 12px; + padding: 2.5rem; + width: 100%; + max-width: 420px; + text-align: center; } -.auth-card .ascii-logo { - margin-bottom: 1.5rem; +.auth-card .logo { + margin-bottom: 1.5rem; } .auth-card h2 { - font-size: 1.5rem; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 1.5rem; + font-size: 1.5rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 1.5rem; } .auth-form { - display: flex; - flex-direction: column; - gap: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; } .form-group { - text-align: left; + text-align: left; } .form-group label { - display: block; - font-size: 0.875rem; - color: var(--text-secondary); - margin-bottom: 0.5rem; + display: block; + font-size: 0.875rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; } .form-group input { - width: 100%; - padding: 0.75rem 1rem; - background: var(--bg-primary); - border: 1px solid var(--border); - border-radius: 8px; - color: var(--text-primary); - font-size: 1rem; - font-family: inherit; - transition: border-color 0.2s, box-shadow 0.2s; + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 1rem; + font-family: inherit; + transition: border-color 0.2s, box-shadow 0.2s; } .form-group input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15); + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15); } .form-group input::placeholder { - color: var(--text-muted); + color: var(--text-muted); } .auth-btn { - width: 100%; - padding: 0.875rem 1.5rem; - background: var(--accent); - color: var(--bg-primary); - border: none; - border-radius: 8px; - font-size: 1rem; - font-weight: 600; - font-family: inherit; - cursor: pointer; - transition: opacity 0.2s, transform 0.1s; - margin-top: 0.5rem; + width: 100%; + padding: 0.875rem 1.5rem; + background: var(--accent); + color: var(--bg-primary); + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + font-family: inherit; + cursor: pointer; + transition: opacity 0.2s, transform 0.1s; + margin-top: 0.5rem; } .auth-btn:hover:not(:disabled) { - opacity: 0.9; + opacity: 0.9; } .auth-btn:active:not(:disabled) { - transform: scale(0.98); + transform: scale(0.98); } .auth-btn:disabled { - opacity: 0.6; - cursor: not-allowed; + opacity: 0.6; + cursor: not-allowed; } .auth-error { - background: var(--red-fill); - border: 1px solid var(--red); - color: var(--red); - padding: 0.75rem 1rem; - border-radius: 8px; - font-size: 0.875rem; - margin-bottom: 1rem; + background: var(--red-fill); + border: 1px solid var(--red); + color: var(--red); + padding: 0.75rem 1rem; + border-radius: 8px; + font-size: 0.875rem; + margin-bottom: 1rem; } .auth-success { - background: var(--green-fill); - border: 1px solid var(--green); - color: var(--green); - padding: 0.75rem 1rem; - border-radius: 8px; - font-size: 0.875rem; - margin-bottom: 1rem; + background: var(--green-fill); + border: 1px solid var(--green); + color: var(--green); + padding: 0.75rem 1rem; + border-radius: 8px; + font-size: 0.875rem; + margin-bottom: 1rem; } .auth-link { - margin-top: 1.5rem; - color: var(--text-secondary); - font-size: 0.875rem; + margin-top: 1.5rem; + color: var(--text-secondary); + font-size: 0.875rem; } .auth-link a { - color: var(--accent); - text-decoration: none; - font-weight: 600; + color: var(--accent); + text-decoration: none; + font-weight: 600; } .auth-link a:hover { - text-decoration: underline; + text-decoration: underline; } /* Waitlist Message */ .waitlist-message { - background: var(--bg-tertiary); - border: 1px solid var(--border); - border-radius: 8px; - padding: 1.5rem; - margin: 1rem 0; - text-align: left; - line-height: 1.6; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1.5rem; + margin: 1rem 0; + text-align: left; + line-height: 1.6; } .waitlist-message p { - color: var(--text-secondary); - margin-bottom: 1rem; + color: var(--text-secondary); + margin-bottom: 1rem; } .waitlist-message .thank-you { - color: var(--accent); - font-weight: 600; - text-align: center; - margin-bottom: 0; + color: var(--accent); + font-weight: 600; + text-align: center; + margin-bottom: 0; } /* Logout Button */ .logout-btn { - position: absolute; - top: 1rem; - right: 1rem; - padding: 0.5rem 1rem; - background: transparent; - border: 1px solid var(--border); - border-radius: 6px; - color: var(--text-secondary); - font-size: 0.875rem; - font-family: inherit; - cursor: pointer; - transition: border-color 0.2s, color 0.2s; + height: 40px; + width: 110px; + font-size: 1.2rem; + font-family: Ac437_Rainbow100_re_132; + position: absolute; + top: 1rem; + right: 1rem; + padding: 0.5rem 1rem; + background: transparent; + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-secondary); + font-family: inherit; + cursor: pointer; + transition: border-color 0.2s, color 0.2s; } .logout-btn:hover { - border-color: var(--red); - color: var(--red); + border-color: var(--red); + color: var(--red); } header { - position: relative; -} + position: relative; +} \ No newline at end of file diff --git a/trahn-trade-frontend/src/api/tradingApi.js b/trahn-trade-frontend/src/api/tradingApi.js index f1d1ba9..b3fbc6a 100644 --- a/trahn-trade-frontend/src/api/tradingApi.js +++ b/trahn-trade-frontend/src/api/tradingApi.js @@ -3,7 +3,7 @@ * Communicates with backend REST API */ -const API_BASE = 'http://localhost:3001/api'; +const API_BASE = 'http://localhost:3001/v1'; export const tradingApi = { /** diff --git a/trahn-trade-frontend/src/components/Chart.jsx b/trahn-trade-frontend/src/components/Chart.jsx index 649fa97..3da40a6 100644 --- a/trahn-trade-frontend/src/components/Chart.jsx +++ b/trahn-trade-frontend/src/components/Chart.jsx @@ -1,350 +1,340 @@ import { useRef, useEffect, useState } from 'react'; const COLORS = { - line: '#58a6ff', - green: '#3fb950', - greenFill: 'rgba(63, 185, 80, 0.12)', - red: '#f85149', - redFill: 'rgba(248, 81, 73, 0.12)', - grid: '#21262d', - text: '#8b949e', - textMuted: '#484f58', - baseline: '#30363d', - buy: '#f0c000', - sell: '#db6d28', - bg: '#0d1117', + line: '#58a6ff', + green: '#3fb950', + greenFill: 'rgba(63, 185, 80, 0.12)', + red: '#f85149', + redFill: 'rgba(248, 81, 73, 0.12)', + grid: '#21262d', + text: '#8b949e', + textMuted: '#484f58', + baseline: '#30363d', + buy: '#f0c000', + sell: '#db6d28', + bg: '#0d1117', }; const PADDING = { top: 20, right: 80, bottom: 40, left: 20 }; export function Chart({ prices, trades }) { - const canvasRef = useRef(null); - const containerRef = useRef(null); - const [tooltip, setTooltip] = useState(null); - const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + const canvasRef = useRef(null); + const containerRef = useRef(null); + const [tooltip, setTooltip] = useState(null); + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); - // Handle resize - useEffect(() => { - const updateDimensions = () => { - if (containerRef.current) { - const rect = containerRef.current.getBoundingClientRect(); - setDimensions({ width: rect.width, height: rect.height }); - } - }; - - updateDimensions(); - window.addEventListener('resize', updateDimensions); - return () => window.removeEventListener('resize', updateDimensions); - }, []); - - // Draw chart - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas || dimensions.width === 0) return; - - const ctx = canvas.getContext('2d'); - const dpr = window.devicePixelRatio || 1; - - canvas.width = dimensions.width * dpr; - canvas.height = dimensions.height * dpr; - ctx.scale(dpr, dpr); - - const width = dimensions.width; - const height = dimensions.height; - const chartWidth = width - PADDING.left - PADDING.right; - const chartHeight = height - PADDING.top - PADDING.bottom; - - // Clear - ctx.clearRect(0, 0, width, height); - - if (!prices || prices.length === 0) { - ctx.fillStyle = COLORS.textMuted; - ctx.font = '14px JetBrains Mono, monospace'; - ctx.textAlign = 'center'; - ctx.fillText('No data for this day', width / 2, height / 2); - return; - } - - // Calculate bounds - const priceValues = prices.map(d => d.p || d.price); - const times = prices.map(d => d.t || d.timestamp); - - let minPrice = Math.min(...priceValues); - let maxPrice = Math.max(...priceValues); - const minTime = Math.min(...times); - const maxTime = Math.max(...times); - - const pricePadding = (maxPrice - minPrice) * 0.05 || 10; - minPrice -= pricePadding; - maxPrice += pricePadding; - - const baselinePrice = priceValues[0]; - - // Coordinate converters - const priceToY = (price) => { - const range = maxPrice - minPrice; - const normalized = (price - minPrice) / range; - return PADDING.top + chartHeight * (1 - normalized); - }; - - const timeToX = (timestamp) => { - const range = maxTime - minTime; - const normalized = (timestamp - minTime) / range; - return PADDING.left + chartWidth * normalized; - }; - - // Draw grid - ctx.strokeStyle = COLORS.grid; - ctx.lineWidth = 1; - - const priceStep = calculateNiceStep(maxPrice - minPrice, 5); - const startPrice = Math.ceil(minPrice / priceStep) * priceStep; - - for (let price = startPrice; price <= maxPrice; price += priceStep) { - const y = priceToY(price); - ctx.beginPath(); - ctx.moveTo(PADDING.left, y); - ctx.lineTo(width - PADDING.right, y); - ctx.stroke(); - } - - // Draw baseline - const baselineY = priceToY(baselinePrice); - ctx.strokeStyle = COLORS.baseline; - ctx.setLineDash([4, 4]); - ctx.beginPath(); - ctx.moveTo(PADDING.left, baselineY); - ctx.lineTo(width - PADDING.right, baselineY); - ctx.stroke(); - ctx.setLineDash([]); - - // Draw gradient fill - if (prices.length >= 2) { - ctx.beginPath(); - ctx.moveTo(timeToX(times[0]), baselineY); - ctx.lineTo(timeToX(times[0]), priceToY(priceValues[0])); - - for (let i = 1; i < prices.length; i++) { - ctx.lineTo(timeToX(times[i]), priceToY(priceValues[i])); - } - - ctx.lineTo(timeToX(times[times.length - 1]), baselineY); - ctx.closePath(); - - const gradient = ctx.createLinearGradient(0, PADDING.top, 0, height - PADDING.bottom); - gradient.addColorStop(0, COLORS.greenFill); - gradient.addColorStop(0.5, 'rgba(0,0,0,0)'); - gradient.addColorStop(1, COLORS.redFill); - ctx.fillStyle = gradient; - ctx.fill(); - } - - // Draw price line - if (prices.length >= 2) { - ctx.beginPath(); - ctx.strokeStyle = COLORS.line; - ctx.lineWidth = 2; - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; - - ctx.moveTo(timeToX(times[0]), priceToY(priceValues[0])); - for (let i = 1; i < prices.length; i++) { - ctx.lineTo(timeToX(times[i]), priceToY(priceValues[i])); - } - ctx.stroke(); - } - - // Draw Y axis labels - ctx.fillStyle = COLORS.text; - ctx.font = '11px JetBrains Mono, monospace'; - ctx.textAlign = 'left'; - - for (let price = startPrice; price <= maxPrice; price += priceStep) { - const y = priceToY(price); - ctx.fillText(formatPrice(price), width - PADDING.right + 8, y + 4); - } - - // Draw X axis labels - ctx.textAlign = 'center'; - const hourMs = 60 * 60 * 1000; - const rangeMs = maxTime - minTime; - let stepMs = hourMs * 4; - if (rangeMs < hourMs * 6) stepMs = hourMs; - if (rangeMs < hourMs * 2) stepMs = hourMs / 2; - - let current = Math.ceil(minTime / stepMs) * stepMs; - while (current <= maxTime) { - const x = timeToX(current); - if (x >= PADDING.left && x <= width - PADDING.right) { - const date = new Date(current); - const label = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); - ctx.fillText(label, x, height - PADDING.bottom + 20); - } - current += stepMs; - } - - // Draw trades - for (const trade of (trades || [])) { - const x = timeToX(trade.t || trade.timestamp); - const y = priceToY(trade.price); - - if (x < PADDING.left || x > width - PADDING.right) continue; - - const color = trade.side === 'buy' ? COLORS.buy : COLORS.sell; - const radius = 7; - - // Glow - ctx.beginPath(); - ctx.arc(x, y, radius + 4, 0, Math.PI * 2); - ctx.fillStyle = color + '33'; - ctx.fill(); - - // Outer - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fillStyle = color; - ctx.fill(); - - // Inner - ctx.beginPath(); - ctx.arc(x, y, radius - 3, 0, Math.PI * 2); - ctx.fillStyle = COLORS.bg; - ctx.fill(); - } - - // Draw current price tag - if (prices.length > 0) { - const lastPrice = priceValues[priceValues.length - 1]; - const y = priceToY(lastPrice); - const x = width - PADDING.right; - - const isUp = lastPrice >= baselinePrice; - const color = isUp ? COLORS.green : COLORS.red; - - ctx.fillStyle = color; - roundRect(ctx, x + 4, y - 11, 70, 22, 4); - ctx.fill(); - - ctx.fillStyle = '#fff'; - ctx.font = 'bold 11px JetBrains Mono, monospace'; - ctx.textAlign = 'left'; - ctx.fillText(formatPrice(lastPrice), x + 10, y + 4); - } - - }, [prices, trades, dimensions]); - - // Mouse handling for tooltip - const handleMouseMove = (e) => { - if (!prices || prices.length === 0) { - setTooltip(null); - return; - } - - const rect = canvasRef.current.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - const width = dimensions.width; - const height = dimensions.height; - const chartWidth = width - PADDING.left - PADDING.right; - - if (x < PADDING.left || x > width - PADDING.right || - y < PADDING.top || y > height - PADDING.bottom) { - setTooltip(null); - return; - } - - const times = prices.map(d => d.t || d.timestamp); - const minTime = Math.min(...times); - const maxTime = Math.max(...times); - - const normalized = (x - PADDING.left) / chartWidth; - const timestamp = minTime + normalized * (maxTime - minTime); - - let nearestPoint = null; - let nearestDistance = Infinity; - - for (const point of prices) { - const t = point.t || point.timestamp; - const distance = Math.abs(t - timestamp); - if (distance < nearestDistance) { - nearestDistance = distance; - nearestPoint = point; - } - } - - if (nearestPoint) { - setTooltip({ - x: e.clientX - rect.left + 15, - y: e.clientY - rect.top - 10, - price: nearestPoint.p || nearestPoint.price, - timestamp: nearestPoint.t || nearestPoint.timestamp, - }); - } + // Handle resize + useEffect(() => { + const updateDimensions = () => { + if (containerRef.current) { + const rect = containerRef.current.getBoundingClientRect(); + setDimensions({ width: rect.width, height: rect.height }); + } }; - return ( -
- setTooltip(null)} - /> - {tooltip && ( -
-
- ${tooltip.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -
-
- {new Date(tooltip.timestamp).toLocaleString('en-US', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - })} -
-
- )} + updateDimensions(); + window.addEventListener('resize', updateDimensions); + return () => window.removeEventListener('resize', updateDimensions); + }, []); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas || dimensions.width === 0) return; + + const ctx = canvas.getContext('2d'); + const dpr = window.devicePixelRatio || 1; + + canvas.width = dimensions.width * dpr; + canvas.height = dimensions.height * dpr; + ctx.scale(dpr, dpr); + + const width = dimensions.width; + const height = dimensions.height; + const chartWidth = width - PADDING.left - PADDING.right; + const chartHeight = height - PADDING.top - PADDING.bottom; + + // Clear + ctx.clearRect(0, 0, width, height); + + if (!prices || prices.length === 0) { + ctx.fillStyle = COLORS.textMuted; + ctx.font = '18px Ac437_Rainbow100_re_132, monospace'; + ctx.textAlign = 'center'; + ctx.fillText('No data for this day', width / 2, height / 2); + return; + } + + // Calculate bounds + const priceValues = prices.map(d => d.p || d.price); + const times = prices.map(d => d.t || d.timestamp); + + let minPrice = Math.min(...priceValues); + let maxPrice = Math.max(...priceValues); + const minTime = Math.min(...times); + const maxTime = Math.max(...times); + + const pricePadding = (maxPrice - minPrice) * 0.05 || 10; + minPrice -= pricePadding; + maxPrice += pricePadding; + + const baselinePrice = priceValues[0]; + + // Coordinate converters + const priceToY = (price) => { + const range = maxPrice - minPrice; + const normalized = (price - minPrice) / range; + return PADDING.top + chartHeight * (1 - normalized); + }; + + const timeToX = (timestamp) => { + const range = maxTime - minTime; + const normalized = (timestamp - minTime) / range; + return PADDING.left + chartWidth * normalized; + }; + + // Draw grid + ctx.strokeStyle = COLORS.grid; + ctx.lineWidth = 1; + + const priceStep = calculateNiceStep(maxPrice - minPrice, 5); + const startPrice = Math.ceil(minPrice / priceStep) * priceStep; + + for (let price = startPrice; price <= maxPrice; price += priceStep) { + const y = priceToY(price); + ctx.beginPath(); + ctx.moveTo(PADDING.left, y); + ctx.lineTo(width - PADDING.right, y); + ctx.stroke(); + } + + const baselineY = priceToY(baselinePrice); + ctx.strokeStyle = COLORS.baseline; + ctx.setLineDash([4, 4]); + ctx.beginPath(); + ctx.moveTo(PADDING.left, baselineY); + ctx.lineTo(width - PADDING.right, baselineY); + ctx.stroke(); + ctx.setLineDash([]); + + if (prices.length >= 2) { + ctx.beginPath(); + ctx.moveTo(timeToX(times[0]), baselineY); + ctx.lineTo(timeToX(times[0]), priceToY(priceValues[0])); + + for (let i = 1; i < prices.length; i++) { + ctx.lineTo(timeToX(times[i]), priceToY(priceValues[i])); + } + + ctx.lineTo(timeToX(times[times.length - 1]), baselineY); + ctx.closePath(); + + const gradient = ctx.createLinearGradient(0, PADDING.top, 0, height - PADDING.bottom); + gradient.addColorStop(0, COLORS.greenFill); + gradient.addColorStop(0.5, 'rgba(0,0,0,0)'); + gradient.addColorStop(1, COLORS.redFill); + ctx.fillStyle = gradient; + ctx.fill(); + } + + if (prices.length >= 2) { + ctx.beginPath(); + ctx.strokeStyle = COLORS.line; + ctx.lineWidth = 2; + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + + ctx.moveTo(timeToX(times[0]), priceToY(priceValues[0])); + for (let i = 1; i < prices.length; i++) { + ctx.lineTo(timeToX(times[i]), priceToY(priceValues[i])); + } + ctx.stroke(); + } + + ctx.fillStyle = COLORS.text; + ctx.font = '14px Ac437_Rainbow100_re_132, monospace'; + ctx.textAlign = 'left'; + + for (let price = startPrice; price <= maxPrice; price += priceStep) { + const y = priceToY(price); + ctx.fillText(formatPrice(price), width - PADDING.right + 8, y + 4); + } + + ctx.textAlign = 'center'; + const hourMs = 60 * 60 * 1000; + const rangeMs = maxTime - minTime; + let stepMs = hourMs * 4; + if (rangeMs < hourMs * 6) stepMs = hourMs; + if (rangeMs < hourMs * 2) stepMs = hourMs / 2; + + let current = Math.ceil(minTime / stepMs) * stepMs; + while (current <= maxTime) { + const x = timeToX(current); + if (x >= PADDING.left && x <= width - PADDING.right) { + const date = new Date(current); + const label = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); + ctx.fillText(label, x, height - PADDING.bottom + 20); + } + current += stepMs; + } + + for (const trade of (trades || [])) { + const x = timeToX(trade.t || trade.timestamp); + const y = priceToY(trade.price); + + if (x < PADDING.left || x > width - PADDING.right) continue; + + const color = trade.side === 'buy' ? COLORS.buy : COLORS.sell; + const radius = 7; + + ctx.beginPath(); + ctx.arc(x, y, radius + 4, 0, Math.PI * 2); + ctx.fillStyle = color + '33'; + ctx.fill(); + + // Outer + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); + + // Inner + ctx.beginPath(); + ctx.arc(x, y, radius - 3, 0, Math.PI * 2); + ctx.fillStyle = COLORS.bg; + ctx.fill(); + } + + if (prices.length > 0) { + const lastPrice = priceValues[priceValues.length - 1]; + const y = priceToY(lastPrice); + const x = width - PADDING.right; + + const isUp = lastPrice >= baselinePrice; + const color = isUp ? COLORS.green : COLORS.red; + + ctx.fillStyle = color; + roundRect(ctx, x + 4, y - 11, 70, 22, 4); + ctx.fill(); + + ctx.fillStyle = '#fff'; + ctx.font = 'bold 14px Ac437_Rainbow100_re_132, monospace'; + ctx.textAlign = 'left'; + ctx.fillText(formatPrice(lastPrice), x + 10, y + 4); + } + + }, [prices, trades, dimensions]); + + const handleMouseMove = (e) => { + if (!prices || prices.length === 0) { + setTooltip(null); + return; + } + + const rect = canvasRef.current.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const width = dimensions.width; + const height = dimensions.height; + const chartWidth = width - PADDING.left - PADDING.right; + + if (x < PADDING.left || x > width - PADDING.right || + y < PADDING.top || y > height - PADDING.bottom) { + setTooltip(null); + return; + } + + const times = prices.map(d => d.t || d.timestamp); + const minTime = Math.min(...times); + const maxTime = Math.max(...times); + + const normalized = (x - PADDING.left) / chartWidth; + const timestamp = minTime + normalized * (maxTime - minTime); + + let nearestPoint = null; + let nearestDistance = Infinity; + + for (const point of prices) { + const t = point.t || point.timestamp; + const distance = Math.abs(t - timestamp); + if (distance < nearestDistance) { + nearestDistance = distance; + nearestPoint = point; + } + } + + if (nearestPoint) { + setTooltip({ + x: e.clientX - rect.left + 15, + y: e.clientY - rect.top - 10, + price: nearestPoint.p || nearestPoint.price, + timestamp: nearestPoint.t || nearestPoint.timestamp, + }); + } + }; + + return ( +
+ setTooltip(null)} + /> + {tooltip && ( +
+
+ ${tooltip.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +
+
+ {new Date(tooltip.timestamp).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +
- ); + )} +
+ ); } function calculateNiceStep(range, targetSteps) { - const roughStep = range / targetSteps; - const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep))); - const normalized = roughStep / magnitude; - - let niceStep; - if (normalized <= 1.5) niceStep = 1; - else if (normalized <= 3) niceStep = 2; - else if (normalized <= 7) niceStep = 5; - else niceStep = 10; - - return niceStep * magnitude; + const roughStep = range / targetSteps; + const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep))); + const normalized = roughStep / magnitude; + + let niceStep; + if (normalized <= 1.5) niceStep = 1; + else if (normalized <= 3) niceStep = 2; + else if (normalized <= 7) niceStep = 5; + else niceStep = 10; + + return niceStep * magnitude; } function formatPrice(price) { - if (price >= 10000) { - return '$' + (price / 1000).toFixed(1) + 'k'; - } - return '$' + price.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); + if (price >= 10000) { + return '$' + (price / 1000).toFixed(1) + 'k'; + } + return '$' + price.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); } function roundRect(ctx, x, y, width, height, radius) { - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); } diff --git a/trahn-trade-frontend/src/fonts/Ac437_Rainbow100_re_132.ttf b/trahn-trade-frontend/src/fonts/Ac437_Rainbow100_re_132.ttf new file mode 100644 index 0000000..3bce328 Binary files /dev/null and b/trahn-trade-frontend/src/fonts/Ac437_Rainbow100_re_132.ttf differ diff --git a/trahn-trade-frontend/src/fonts/Mx437_Compaq_Port3.ttf b/trahn-trade-frontend/src/fonts/Mx437_Compaq_Port3.ttf new file mode 100644 index 0000000..9be9155 Binary files /dev/null and b/trahn-trade-frontend/src/fonts/Mx437_Compaq_Port3.ttf differ diff --git a/trahn-trade-frontend/src/fonts/Px437_AMI_EGA_8x8.ttf b/trahn-trade-frontend/src/fonts/Px437_AMI_EGA_8x8.ttf new file mode 100644 index 0000000..2962a72 Binary files /dev/null and b/trahn-trade-frontend/src/fonts/Px437_AMI_EGA_8x8.ttf differ diff --git a/trahn-trade-frontend/src/fonts/Px437_Robotron_A7100.ttf b/trahn-trade-frontend/src/fonts/Px437_Robotron_A7100.ttf new file mode 100644 index 0000000..7107a2a Binary files /dev/null and b/trahn-trade-frontend/src/fonts/Px437_Robotron_A7100.ttf differ diff --git a/trahn-trade-frontend/src/index.css b/trahn-trade-frontend/src/index.css index ea1372d..8114101 100644 --- a/trahn-trade-frontend/src/index.css +++ b/trahn-trade-frontend/src/index.css @@ -1 +1,34 @@ -/* Reset handled in App.css */ +@font-face { + font-family: 'Ac437_Rainbow100_re_1323'; + src: local('./fonts/Mx437_Compaq_Port3.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Ac437_Rainbow100_re_1323'; + src: local('./fonts/Mx437_Compaq_Port3.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Px437_AMI_EGA_8x8'; + src: local('Px437_AMI_EGA_8x8'), url(./fonts/Px437_AMI_EGA_8x8.ttf) format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Ac437_Rainbow100_re_132'; + src: local('Ac437_Rainbow100_re_132'), url(./fonts/Ac437_Rainbow100_re_132.ttf) format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Ac437_Rainbow100_re_132'; + src: local('Ac437_Rainbow100_re_132'), url(./fonts/Ac437_Rainbow100_re_132.ttf) format('truetype'); + font-weight: normal; + font-style: normal; +} \ No newline at end of file diff --git a/trahn-trade-frontend/src/pages/Dashboard.jsx b/trahn-trade-frontend/src/pages/Dashboard.jsx index 61d1f09..11f78af 100644 --- a/trahn-trade-frontend/src/pages/Dashboard.jsx +++ b/trahn-trade-frontend/src/pages/Dashboard.jsx @@ -52,13 +52,7 @@ export function Dashboard() { return (
-
{`
-   ████████╗██████╗  █████╗ ██╗  ██╗███╗   ██╗
-   ╚══██╔══╝██╔══██╗██╔══██╗██║  ██║████╗  ██║
-      ██║   ██████╔╝███████║███████║██╔██╗ ██║
-      ██║   ██╔══██╗██╔══██║██╔══██║██║╚██╗██║
-      ██║   ██║  ██║██║  ██║██║  ██║██║ ╚████║
-      ╚═╝   ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═══╝`}
+

TRAHN

diff --git a/trahn-trade-frontend/src/pages/Login.jsx b/trahn-trade-frontend/src/pages/Login.jsx index 9f74cc9..7c951d2 100644 --- a/trahn-trade-frontend/src/pages/Login.jsx +++ b/trahn-trade-frontend/src/pages/Login.jsx @@ -29,13 +29,7 @@ export function Login() { return (
-
{`
-   ████████╗██████╗  █████╗ ██╗  ██╗███╗   ██╗
-   ╚══██╔══╝██╔══██╗██╔══██╗██║  ██║████╗  ██║
-      ██║   ██████╔╝███████║███████║██╔██╗ ██║
-      ██║   ██╔══██╗██╔══██║██╔══██║██║╚██╗██║
-      ██║   ██║  ██║██║  ██║██║  ██║██║ ╚████║
-      ╚═╝   ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═══╝`}
+

TRAHN

Welcome Back

diff --git a/trahn-trade-frontend/src/pages/Signup.jsx b/trahn-trade-frontend/src/pages/Signup.jsx index 64e9988..b795cd6 100644 --- a/trahn-trade-frontend/src/pages/Signup.jsx +++ b/trahn-trade-frontend/src/pages/Signup.jsx @@ -38,13 +38,7 @@ export function Signup() { return (
-
{`
-   ████████╗██████╗  █████╗ ██╗  ██╗███╗   ██╗
-   ╚══██╔══╝██╔══██╗██╔══██╗██║  ██║████╗  ██║
-      ██║   ██████╔╝███████║███████║██╔██╗ ██║
-      ██║   ██╔══██╗██╔══██║██╔══██║██║╚██╗██║
-      ██║   ██║  ██║██║  ██║██║  ██║██║ ╚████║
-      ╚═╝   ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═══╝`}
+

TRAHN

{submitted ? ( <>