1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
|||||||
*.key
|
*.key
|
||||||
*.env
|
*.env
|
||||||
*secrets.js
|
*secrets.js
|
||||||
|
secrets.js
|
||||||
trahn-trade-frontend/src/auth/secrets.js*
|
trahn-trade-frontend/src/auth/secrets.js*
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func (r *PriceRepo) GetByDay(ctx context.Context, tradingDay string) ([]models.P
|
|||||||
|
|
||||||
func (r *PriceRepo) GetAvailableDays(ctx context.Context) ([]string, error) {
|
func (r *PriceRepo) GetAvailableDays(ctx context.Context) ([]string, error) {
|
||||||
rows, err := r.pool.Query(ctx,
|
rows, err := r.pool.Query(ctx,
|
||||||
`SELECT DISTINCT trading_day FROM price_history ORDER BY trading_day DESC LIMIT 30`,
|
`SELECT DISTINCT trading_day FROM price_history ORDER BY trading_day ASC LIMIT 30`,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>TRAHN Grid Trader</title>
|
<title>TRAHN Grid Trader</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
:root {
|
:root {
|
||||||
--bg-primary: #0d1117;
|
--bg-primary: #000000;
|
||||||
--bg-secondary: #161b22;
|
--bg-secondary: #070808;
|
||||||
--bg-tertiary: #21262d;
|
--bg-tertiary: #363737;
|
||||||
--text-primary: #e6edf3;
|
--text-primary: #01ff3c;
|
||||||
--text-secondary: #8b949e;
|
--text-secondary: #01ff3c;
|
||||||
--text-muted: #484f58;
|
--text-muted: #484f58;
|
||||||
--accent: #58a6ff;
|
--accent: #58a6ff;
|
||||||
--green: #3fb950;
|
--green: #3fb950;
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: 'Ac437_Rainbow100_re_132', monospace;
|
||||||
|
font-size: 18px;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -35,21 +36,27 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ascii-logo {
|
.logo {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
margin-left: 32px;
|
||||||
font-size: 0.65rem;
|
font-family: 'Ac437_Rainbow100_re_132', monospace;
|
||||||
line-height: 1.1;
|
font-size: 9rem;
|
||||||
color: var(--accent);
|
font-weight: 700;
|
||||||
margin: 0;
|
color: var(--text-primary);
|
||||||
display: inline-block;
|
text-align: center;
|
||||||
|
letter-spacing: 0.25em;
|
||||||
|
text-shadow: 4px 12px 12px rgb(173, 1, 124);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'Ac437_Rainbow100_re_132', monospace;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
@@ -96,16 +103,18 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.date-label {
|
.date-label {
|
||||||
display: block;
|
font-family: 'Ac437_Rainbow100_re_132';
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 1.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 2px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-status {
|
.date-status {
|
||||||
display: block;
|
display: block;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'Ac437_Rainbow100_re_132', monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 1rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -121,8 +130,15 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.4; }
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chart */
|
/* Chart */
|
||||||
@@ -145,7 +161,7 @@ header {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'Ac437_Rainbow100_re_132', monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -184,7 +200,7 @@ header {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'Ac437_Rainbow100_re_132', monospace;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,12 +214,15 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stats */
|
/* Stats */
|
||||||
.stats {
|
.stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
font-size: 1.2rem;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
@@ -211,6 +230,7 @@ header {
|
|||||||
|
|
||||||
.stat {
|
.stat {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
font-size: 1.2rem;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -228,15 +248,26 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'Ac437_Rainbow100_re_132', monospace;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value.high { color: var(--green); }
|
.stat-value.high {
|
||||||
.stat-value.low { color: var(--red); }
|
color: var(--green);
|
||||||
.stat-value.buy { color: var(--yellow); }
|
}
|
||||||
.stat-value.sell { color: var(--orange); }
|
|
||||||
|
.stat-value.low {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.buy {
|
||||||
|
color: var(--yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.sell {
|
||||||
|
color: var(--orange);
|
||||||
|
}
|
||||||
|
|
||||||
/* Day indicators */
|
/* Day indicators */
|
||||||
.day-indicators {
|
.day-indicators {
|
||||||
@@ -276,6 +307,7 @@ header {
|
|||||||
.legend-item {
|
.legend-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-size: 1rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +339,7 @@ header {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'Ac437_Rainbow100_re_132', monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
@@ -363,7 +395,7 @@ header {
|
|||||||
|
|
||||||
.grid-levels-header {
|
.grid-levels-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.875rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
@@ -385,7 +417,7 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grid-column-header {
|
.grid-column-header {
|
||||||
font-size: 0.75rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
@@ -395,18 +427,20 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grid-column-header.buy {
|
.grid-column-header.buy {
|
||||||
|
font-size: 1.2rem;
|
||||||
color: var(--yellow);
|
color: var(--yellow);
|
||||||
background: rgba(240, 192, 0, 0.1);
|
background: rgba(240, 192, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-column-header.sell {
|
.grid-column-header.sell {
|
||||||
|
font-size: 1.2rem;
|
||||||
color: var(--orange);
|
color: var(--orange);
|
||||||
background: rgba(219, 109, 40, 0.1);
|
background: rgba(219, 109, 40, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-level {
|
.grid-level {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'Ac437_Rainbow100_re_132', monospace;
|
||||||
font-size: 0.8rem;
|
font-size: 1.2rem;
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
@@ -415,6 +449,7 @@ header {
|
|||||||
|
|
||||||
.grid-level.buy {
|
.grid-level.buy {
|
||||||
color: var(--yellow);
|
color: var(--yellow);
|
||||||
|
font-size: 1.2rem;
|
||||||
background: rgba(240, 192, 0, 0.08);
|
background: rgba(240, 192, 0, 0.08);
|
||||||
border: 1px solid rgba(240, 192, 0, 0.2);
|
border: 1px solid rgba(240, 192, 0, 0.2);
|
||||||
}
|
}
|
||||||
@@ -449,7 +484,7 @@ header {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-card .ascii-logo {
|
.auth-card .logo {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,6 +623,10 @@ header {
|
|||||||
|
|
||||||
/* Logout Button */
|
/* Logout Button */
|
||||||
.logout-btn {
|
.logout-btn {
|
||||||
|
height: 40px;
|
||||||
|
width: 110px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-family: Ac437_Rainbow100_re_132;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
@@ -596,7 +635,6 @@ header {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.875rem;
|
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.2s, color 0.2s;
|
transition: border-color 0.2s, color 0.2s;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { Login } from './pages/Login';
|
|||||||
import { Signup } from './pages/Signup';
|
import { Signup } from './pages/Signup';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
// Create React Query client
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
@@ -20,7 +19,6 @@ const queryClient = new QueryClient({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect authenticated users away from auth pages
|
|
||||||
function PublicRoute({ children }) {
|
function PublicRoute({ children }) {
|
||||||
const { currentUser } = useAuth();
|
const { currentUser } = useAuth();
|
||||||
|
|
||||||
|
|||||||
@@ -1,77 +1,50 @@
|
|||||||
/**
|
const API_BASE = 'http://localhost:3001/v1';
|
||||||
* Trading API Client
|
|
||||||
* Communicates with backend REST API
|
|
||||||
*/
|
|
||||||
|
|
||||||
const API_BASE = 'http://localhost:3001/api';
|
|
||||||
|
|
||||||
export const tradingApi = {
|
export const tradingApi = {
|
||||||
/**
|
|
||||||
* Get current trading day prices
|
|
||||||
*/
|
|
||||||
getCurrentPrices: async () => {
|
getCurrentPrices: async () => {
|
||||||
const response = await fetch(`${API_BASE}/prices/today`);
|
const response = await fetch(`${API_BASE}/prices/today`);
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current trading day trades
|
|
||||||
*/
|
|
||||||
getCurrentTrades: async () => {
|
getCurrentTrades: async () => {
|
||||||
const response = await fetch(`${API_BASE}/trades/today`);
|
const response = await fetch(`${API_BASE}/trades/today`);
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current grid state
|
|
||||||
*/
|
|
||||||
getCurrentGrid: async () => {
|
getCurrentGrid: async () => {
|
||||||
const response = await fetch(`${API_BASE}/grid/current`);
|
const response = await fetch(`${API_BASE}/grid/current`);
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get latest support/resistance levels
|
|
||||||
*/
|
|
||||||
getLatestSR: async () => {
|
getLatestSR: async () => {
|
||||||
const response = await fetch(`${API_BASE}/support-resistance/latest`);
|
const response = await fetch(`${API_BASE}/support-resistance/latest`);
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get available trading days
|
|
||||||
*/
|
|
||||||
getAvailableDays: async () => {
|
getAvailableDays: async () => {
|
||||||
const response = await fetch(`${API_BASE}/prices/days`);
|
const response = await fetch(`${API_BASE}/prices/days`);
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get historical prices for a specific day
|
|
||||||
*/
|
|
||||||
getPricesByDay: async (date) => {
|
getPricesByDay: async (date) => {
|
||||||
const response = await fetch(`${API_BASE}/prices/day/${date}`);
|
const response = await fetch(`${API_BASE}/prices/day/${date}`);
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get historical trades for a specific day
|
|
||||||
*/
|
|
||||||
getTradesByDay: async (date) => {
|
getTradesByDay: async (date) => {
|
||||||
const response = await fetch(`${API_BASE}/trades/day/${date}`);
|
const response = await fetch(`${API_BASE}/trades/day/${date}`);
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get trade statistics
|
|
||||||
*/
|
|
||||||
getTradeStats: async () => {
|
getTradeStats: async () => {
|
||||||
const response = await fetch(`${API_BASE}/trades/stats`);
|
const response = await fetch(`${API_BASE}/trades/stats`);
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const app = initializeApp(firebaseConfig);
|
|||||||
export const auth = getAuth(app);
|
export const auth = getAuth(app);
|
||||||
export const db = getFirestore(app);
|
export const db = getFirestore(app);
|
||||||
|
|
||||||
// Add email to waitlist collection
|
|
||||||
export async function addToWaitlist(email) {
|
export async function addToWaitlist(email) {
|
||||||
try {
|
try {
|
||||||
await addDoc(collection(db, 'trahn-trade-signups'), {
|
await addDoc(collection(db, 'trahn-trade-signups'), {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export function Chart({ prices, trades }) {
|
|||||||
const [tooltip, setTooltip] = useState(null);
|
const [tooltip, setTooltip] = useState(null);
|
||||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
// Handle resize
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateDimensions = () => {
|
const updateDimensions = () => {
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
@@ -37,7 +36,6 @@ export function Chart({ prices, trades }) {
|
|||||||
return () => window.removeEventListener('resize', updateDimensions);
|
return () => window.removeEventListener('resize', updateDimensions);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Draw chart
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas || dimensions.width === 0) return;
|
if (!canvas || dimensions.width === 0) return;
|
||||||
@@ -54,18 +52,16 @@ export function Chart({ prices, trades }) {
|
|||||||
const chartWidth = width - PADDING.left - PADDING.right;
|
const chartWidth = width - PADDING.left - PADDING.right;
|
||||||
const chartHeight = height - PADDING.top - PADDING.bottom;
|
const chartHeight = height - PADDING.top - PADDING.bottom;
|
||||||
|
|
||||||
// Clear
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
if (!prices || prices.length === 0) {
|
if (!prices || prices.length === 0) {
|
||||||
ctx.fillStyle = COLORS.textMuted;
|
ctx.fillStyle = COLORS.textMuted;
|
||||||
ctx.font = '14px JetBrains Mono, monospace';
|
ctx.font = '18px Ac437_Rainbow100_re_132, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText('No data for this day', width / 2, height / 2);
|
ctx.fillText('No data for this day', width / 2, height / 2);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate bounds
|
|
||||||
const priceValues = prices.map(d => d.p || d.price);
|
const priceValues = prices.map(d => d.p || d.price);
|
||||||
const times = prices.map(d => d.t || d.timestamp);
|
const times = prices.map(d => d.t || d.timestamp);
|
||||||
|
|
||||||
@@ -80,7 +76,6 @@ export function Chart({ prices, trades }) {
|
|||||||
|
|
||||||
const baselinePrice = priceValues[0];
|
const baselinePrice = priceValues[0];
|
||||||
|
|
||||||
// Coordinate converters
|
|
||||||
const priceToY = (price) => {
|
const priceToY = (price) => {
|
||||||
const range = maxPrice - minPrice;
|
const range = maxPrice - minPrice;
|
||||||
const normalized = (price - minPrice) / range;
|
const normalized = (price - minPrice) / range;
|
||||||
@@ -93,7 +88,6 @@ export function Chart({ prices, trades }) {
|
|||||||
return PADDING.left + chartWidth * normalized;
|
return PADDING.left + chartWidth * normalized;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw grid
|
|
||||||
ctx.strokeStyle = COLORS.grid;
|
ctx.strokeStyle = COLORS.grid;
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
|
|
||||||
@@ -108,7 +102,6 @@ export function Chart({ prices, trades }) {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw baseline
|
|
||||||
const baselineY = priceToY(baselinePrice);
|
const baselineY = priceToY(baselinePrice);
|
||||||
ctx.strokeStyle = COLORS.baseline;
|
ctx.strokeStyle = COLORS.baseline;
|
||||||
ctx.setLineDash([4, 4]);
|
ctx.setLineDash([4, 4]);
|
||||||
@@ -118,7 +111,6 @@ export function Chart({ prices, trades }) {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
// Draw gradient fill
|
|
||||||
if (prices.length >= 2) {
|
if (prices.length >= 2) {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(timeToX(times[0]), baselineY);
|
ctx.moveTo(timeToX(times[0]), baselineY);
|
||||||
@@ -139,7 +131,6 @@ export function Chart({ prices, trades }) {
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw price line
|
|
||||||
if (prices.length >= 2) {
|
if (prices.length >= 2) {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.strokeStyle = COLORS.line;
|
ctx.strokeStyle = COLORS.line;
|
||||||
@@ -154,9 +145,8 @@ export function Chart({ prices, trades }) {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Y axis labels
|
|
||||||
ctx.fillStyle = COLORS.text;
|
ctx.fillStyle = COLORS.text;
|
||||||
ctx.font = '11px JetBrains Mono, monospace';
|
ctx.font = '14px Ac437_Rainbow100_re_132, monospace';
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
|
|
||||||
for (let price = startPrice; price <= maxPrice; price += priceStep) {
|
for (let price = startPrice; price <= maxPrice; price += priceStep) {
|
||||||
@@ -164,7 +154,6 @@ export function Chart({ prices, trades }) {
|
|||||||
ctx.fillText(formatPrice(price), width - PADDING.right + 8, y + 4);
|
ctx.fillText(formatPrice(price), width - PADDING.right + 8, y + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw X axis labels
|
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
const hourMs = 60 * 60 * 1000;
|
const hourMs = 60 * 60 * 1000;
|
||||||
const rangeMs = maxTime - minTime;
|
const rangeMs = maxTime - minTime;
|
||||||
@@ -183,7 +172,6 @@ export function Chart({ prices, trades }) {
|
|||||||
current += stepMs;
|
current += stepMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw trades
|
|
||||||
for (const trade of (trades || [])) {
|
for (const trade of (trades || [])) {
|
||||||
const x = timeToX(trade.t || trade.timestamp);
|
const x = timeToX(trade.t || trade.timestamp);
|
||||||
const y = priceToY(trade.price);
|
const y = priceToY(trade.price);
|
||||||
@@ -193,26 +181,22 @@ export function Chart({ prices, trades }) {
|
|||||||
const color = trade.side === 'buy' ? COLORS.buy : COLORS.sell;
|
const color = trade.side === 'buy' ? COLORS.buy : COLORS.sell;
|
||||||
const radius = 7;
|
const radius = 7;
|
||||||
|
|
||||||
// Glow
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, radius + 4, 0, Math.PI * 2);
|
ctx.arc(x, y, radius + 4, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = color + '33';
|
ctx.fillStyle = color + '33';
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Outer
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Inner
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, radius - 3, 0, Math.PI * 2);
|
ctx.arc(x, y, radius - 3, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = COLORS.bg;
|
ctx.fillStyle = COLORS.bg;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw current price tag
|
|
||||||
if (prices.length > 0) {
|
if (prices.length > 0) {
|
||||||
const lastPrice = priceValues[priceValues.length - 1];
|
const lastPrice = priceValues[priceValues.length - 1];
|
||||||
const y = priceToY(lastPrice);
|
const y = priceToY(lastPrice);
|
||||||
@@ -226,14 +210,13 @@ export function Chart({ prices, trades }) {
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
ctx.fillStyle = '#fff';
|
ctx.fillStyle = '#fff';
|
||||||
ctx.font = 'bold 11px JetBrains Mono, monospace';
|
ctx.font = 'bold 14px Ac437_Rainbow100_re_132, monospace';
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.fillText(formatPrice(lastPrice), x + 10, y + 4);
|
ctx.fillText(formatPrice(lastPrice), x + 10, y + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [prices, trades, dimensions]);
|
}, [prices, trades, dimensions]);
|
||||||
|
|
||||||
// Mouse handling for tooltip
|
|
||||||
const handleMouseMove = (e) => {
|
const handleMouseMove = (e) => {
|
||||||
if (!prices || prices.length === 0) {
|
if (!prices || prices.length === 0) {
|
||||||
setTooltip(null);
|
setTooltip(null);
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ function reducer(state, action) {
|
|||||||
export function TradingDataProvider({ children }) {
|
export function TradingDataProvider({ children }) {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
|
||||||
// React Query: Poll current day data every 1 minute
|
|
||||||
const { data: currentData, error, isLoading } = useQuery({
|
const { data: currentData, error, isLoading } = useQuery({
|
||||||
queryKey: ['currentDay'],
|
queryKey: ['currentDay'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -87,7 +86,6 @@ export function TradingDataProvider({ children }) {
|
|||||||
retryDelay: 2000,
|
retryDelay: 2000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update context when live data changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentData) {
|
if (currentData) {
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -97,7 +95,6 @@ export function TradingDataProvider({ children }) {
|
|||||||
}
|
}
|
||||||
}, [currentData]);
|
}, [currentData]);
|
||||||
|
|
||||||
// Handle connection status
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
dispatch({ type: 'SET_CONNECTION_STATUS', status: 'error' });
|
dispatch({ type: 'SET_CONNECTION_STATUS', status: 'error' });
|
||||||
|
|||||||
BIN
trahn-trade-frontend/src/fonts/Ac437_Rainbow100_re_132.ttf
Normal file
BIN
trahn-trade-frontend/src/fonts/Ac437_Rainbow100_re_132.ttf
Normal file
Binary file not shown.
BIN
trahn-trade-frontend/src/fonts/Mx437_Compaq_Port3.ttf
Normal file
BIN
trahn-trade-frontend/src/fonts/Mx437_Compaq_Port3.ttf
Normal file
Binary file not shown.
BIN
trahn-trade-frontend/src/fonts/Px437_AMI_EGA_8x8.ttf
Normal file
BIN
trahn-trade-frontend/src/fonts/Px437_AMI_EGA_8x8.ttf
Normal file
Binary file not shown.
BIN
trahn-trade-frontend/src/fonts/Px437_Robotron_A7100.ttf
Normal file
BIN
trahn-trade-frontend/src/fonts/Px437_Robotron_A7100.ttf
Normal file
Binary file not shown.
@@ -1,18 +1,3 @@
|
|||||||
/**
|
|
||||||
* DEPRECATED - Replaced by TradingDataContext + React Query
|
|
||||||
*
|
|
||||||
* This file is kept for reference but no longer used.
|
|
||||||
* The new implementation:
|
|
||||||
* - Uses React Query for polling
|
|
||||||
* - Uses Context for state management
|
|
||||||
* - Calls REST API instead of JSON files
|
|
||||||
*
|
|
||||||
* See:
|
|
||||||
* - contexts/TradingDataContext.jsx
|
|
||||||
* - hooks/useTradingData.js
|
|
||||||
* - api/tradingApi.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function useChartData() {
|
export function useChartData() {
|
||||||
throw new Error('useChartData is deprecated. Use useTradingData from TradingDataContext instead.');
|
throw new Error('useChartData is deprecated. Use useTradingData from TradingDataContext instead.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
/**
|
|
||||||
* Custom hooks for trading data management
|
|
||||||
* Handles carousel navigation and historical day loading
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useTradingData } from '../contexts/TradingDataContext';
|
import { useTradingData } from '../contexts/TradingDataContext';
|
||||||
import { tradingApi } from '../api/tradingApi';
|
import { tradingApi } from '../api/tradingApi';
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for loading historical day data (lazy load)
|
|
||||||
*/
|
|
||||||
export function useHistoricalDay(date, enabled = false) {
|
export function useHistoricalDay(date, enabled = false) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['historicalDay', date],
|
queryKey: ['historicalDay', date],
|
||||||
@@ -26,9 +18,6 @@ export function useHistoricalDay(date, enabled = false) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for carousel navigation
|
|
||||||
*/
|
|
||||||
export function useCarousel() {
|
export function useCarousel() {
|
||||||
const { state, dispatch } = useTradingData();
|
const { state, dispatch } = useTradingData();
|
||||||
|
|
||||||
@@ -38,12 +27,9 @@ export function useCarousel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (index === state.availableDays.length - 1) {
|
if (index === state.availableDays.length - 1) {
|
||||||
// Navigate to live (current) day
|
|
||||||
dispatch({ type: 'SWITCH_TO_LIVE' });
|
dispatch({ type: 'SWITCH_TO_LIVE' });
|
||||||
} else {
|
} else {
|
||||||
// Load historical day
|
|
||||||
const date = state.availableDays[index];
|
const date = state.availableDays[index];
|
||||||
// Historical data will be loaded via useHistoricalDay in Dashboard
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'LOAD_HISTORICAL_DAY',
|
type: 'LOAD_HISTORICAL_DAY',
|
||||||
prices: [],
|
prices: [],
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -12,11 +12,9 @@ export function Dashboard() {
|
|||||||
const { state, dispatch } = useTradingData();
|
const { state, dispatch } = useTradingData();
|
||||||
const carousel = useCarousel();
|
const carousel = useCarousel();
|
||||||
|
|
||||||
// Load historical day data when viewing past days
|
|
||||||
const historicalDate = !state.isLive ? state.availableDays[state.currentDayIndex] : null;
|
const historicalDate = !state.isLive ? state.availableDays[state.currentDayIndex] : null;
|
||||||
const { data: historicalData } = useHistoricalDay(historicalDate, !state.isLive);
|
const { data: historicalData } = useHistoricalDay(historicalDate, !state.isLive);
|
||||||
|
|
||||||
// Update context with historical data when loaded
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (historicalData && !state.isLive) {
|
if (historicalData && !state.isLive) {
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -52,13 +50,7 @@ export function Dashboard() {
|
|||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<header>
|
<header>
|
||||||
<pre className="ascii-logo">{`
|
<h1 className="logo">TRAHN</h1>
|
||||||
████████╗██████╗ █████╗ ██╗ ██╗███╗ ██╗
|
|
||||||
╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║
|
|
||||||
██║ ██████╔╝███████║███████║██╔██╗ ██║
|
|
||||||
██║ ██╔══██╗██╔══██║██╔══██║██║╚██╗██║
|
|
||||||
██║ ██║ ██║██║ ██║██║ ██║██║ ╚████║
|
|
||||||
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝`}</pre>
|
|
||||||
<button className="logout-btn" onClick={logout}>Logout</button>
|
<button className="logout-btn" onClick={logout}>Logout</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -29,13 +29,7 @@ export function Login() {
|
|||||||
return (
|
return (
|
||||||
<div className="auth-container">
|
<div className="auth-container">
|
||||||
<div className="auth-card">
|
<div className="auth-card">
|
||||||
<pre className="ascii-logo">{`
|
<h1 className="logo">TRAHN</h1>
|
||||||
████████╗██████╗ █████╗ ██╗ ██╗███╗ ██╗
|
|
||||||
╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║
|
|
||||||
██║ ██████╔╝███████║███████║██╔██╗ ██║
|
|
||||||
██║ ██╔══██╗██╔══██║██╔══██║██║╚██╗██║
|
|
||||||
██║ ██║ ██║██║ ██║██║ ██║██║ ╚████║
|
|
||||||
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝`}</pre>
|
|
||||||
|
|
||||||
<h2>Welcome Back</h2>
|
<h2>Welcome Back</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { addToWaitlist } from '../auth/firebase';
|
import { addToWaitlist } from '../auth/firebase';
|
||||||
//a comment
|
|
||||||
|
|
||||||
export function Signup() {
|
export function Signup() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -33,18 +30,11 @@ export function Signup() {
|
|||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
// some spacing
|
|
||||||
//more
|
|
||||||
return (
|
return (
|
||||||
<div className="auth-container">
|
<div className="auth-container">
|
||||||
<div className="auth-card">
|
<div className="auth-card">
|
||||||
<pre className="ascii-logo">{`
|
<h1 className="logo">TRAHN</h1>
|
||||||
████████╗██████╗ █████╗ ██╗ ██╗███╗ ██╗
|
|
||||||
╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║
|
|
||||||
██║ ██████╔╝███████║███████║██╔██╗ ██║
|
|
||||||
██║ ██╔══██╗██╔══██║██╔══██║██║╚██╗██║
|
|
||||||
██║ ██║ ██║██║ ██║██║ ██║██║ ╚████║
|
|
||||||
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝`}</pre>
|
|
||||||
|
|
||||||
{submitted ? (
|
{submitted ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user