From b4c212d6ccd25e59f361a054cc118bcbf7c20519 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 3 Feb 2026 02:19:12 +0000 Subject: [PATCH] Autosave: 20260203-021911 --- backend/src/routes/proxy.js | 377 +++++------ .../src/components/Balatro/BalatroGame.tsx | 604 ++++++++++++++++++ frontend/src/components/NavBarItem.tsx | 5 +- frontend/src/layouts/Authenticated.tsx | 5 +- frontend/src/menuAside.ts | 12 +- frontend/src/pages/_app.tsx | 398 +----------- frontend/src/pages/browser.tsx | 304 +++++++++ frontend/src/pages/index.tsx | 334 +--------- 8 files changed, 1110 insertions(+), 929 deletions(-) create mode 100644 frontend/src/components/Balatro/BalatroGame.tsx create mode 100644 frontend/src/pages/browser.tsx diff --git a/backend/src/routes/proxy.js b/backend/src/routes/proxy.js index 15400f5..4f4e34a 100644 --- a/backend/src/routes/proxy.js +++ b/backend/src/routes/proxy.js @@ -4,300 +4,211 @@ const axios = require('axios'); const Helpers = require('../helpers'); const qs = require('qs'); +/** + * Super Stealth Proxy v3 + * Engineered for high-fidelity dynamic site replication (YouTube, Google, etc.) + */ + +const PROXY_PATH = '/api/proxy'; + const proxyHandler = Helpers.wrapAsync(async (req, res) => { let targetUrl = req.query.url; - - if (targetUrl) { - try { - const urlObj = new URL(targetUrl); - Object.keys(req.query).forEach(key => { - if (key !== 'url') { - urlObj.searchParams.set(key, req.query[key]); - } - }); - targetUrl = urlObj.href; - } catch (e) {} - } - // Improved Path-based and Referer detection + // 1. URL Resolution if (!targetUrl || !targetUrl.startsWith('http')) { const originalUrl = req.originalUrl; - - // Check if URL is in the path directly (e.g. /api/proxy/https://...) - const pathMarker = '/api/proxy/'; - if (originalUrl.includes(pathMarker)) { - const parts = originalUrl.split(pathMarker); - const remainder = parts[1]; - if (remainder && remainder.startsWith('http')) { - targetUrl = remainder; - } else if (req.headers.referer && req.headers.referer.includes('url=')) { - // Smart reconstruction from Referer - try { - const refUrl = new URL(req.headers.referer); - const parentTarget = refUrl.searchParams.get('url'); - if (parentTarget) { - // Reconstruct: parent origin + current query/path - const currentPath = req.url.replace('/api/proxy', ''); - targetUrl = new URL(currentPath, parentTarget).href; - } - } catch (e) {} - } + if (originalUrl.includes(PROXY_PATH + '/')) { + targetUrl = originalUrl.split(PROXY_PATH + '/')[1]; + } else if (req.headers.referer && req.headers.referer.includes(PROXY_PATH)) { + try { + const refUrl = new URL(req.headers.referer); + const parentTarget = refUrl.searchParams.get('url'); + if (parentTarget) { + const relativePath = req.url.replace(PROXY_PATH, ''); + targetUrl = new URL(relativePath, parentTarget).href; + } + } catch (e) {} } } if (!targetUrl || !targetUrl.startsWith('http')) { - return res.status(400).send(` -
-

URL Required

-

The stealth proxy needs a target URL to work.

- -
- `); + return res.status(400).send('Invalid target URL.'); } try { - const method = req.method; const headers = {}; - - const forwardHeaders = [ - 'accept', 'accept-language', 'accept-encoding', 'user-agent', 'content-type', 'range', 'authorization', 'x-requested-with' - ]; - - forwardHeaders.forEach(h => { - if (req.headers[h]) { - headers[h] = req.headers[h]; - } - }); + const forwardHeaders = ['accept', 'accept-language', 'range', 'authorization', 'content-type']; + forwardHeaders.forEach(h => { if (req.headers[h]) headers[h] = req.headers[h]; }); - if (!headers['user-agent']) { - headers['user-agent'] = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1'; - } - - delete headers['referer']; - delete headers['origin']; + // Force Desktop User-Agent + headers['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'; + + try { + const targetObj = new URL(targetUrl); + headers['referer'] = targetObj.origin + '/'; + headers['origin'] = targetObj.origin; + headers['host'] = targetObj.host; + } catch(e) {} const axiosConfig = { url: targetUrl, - method: method, + method: req.method, headers: headers, responseType: 'arraybuffer', - validateStatus: () => true, - maxRedirects: 10, + validateStatus: () => true, + maxRedirects: 5, timeout: 30000, + maxContentLength: Infinity, + maxBodyLength: Infinity, decompress: true }; - if (method !== 'GET' && method !== 'HEAD' && req.body) { - if (headers['content-type'] && headers['content-type'].includes('application/x-www-form-urlencoded')) { - axiosConfig.data = typeof req.body === 'string' ? req.body : qs.stringify(req.body); - } else { - axiosConfig.data = req.body; - } + if (req.method !== 'GET' && req.body) { + axiosConfig.data = (headers['content-type'] || '').includes('form') ? qs.stringify(req.body) : req.body; } const response = await axios(axiosConfig); - const finalUrl = response.request.res.responseUrl || targetUrl; const contentType = response.headers['content-type'] || ''; + const finalUrl = response.request.res ? response.request.res.responseUrl : targetUrl; + // Handle Redirects if (response.status >= 300 && response.status < 400 && response.headers.location) { const redirUrl = new URL(response.headers.location, finalUrl).href; - return res.redirect(`/api/proxy?url=${encodeURIComponent(redirUrl)}`); + return res.redirect(`${PROXY_PATH}?url=${encodeURIComponent(redirUrl)}`); } + // Strip Security Headers + const forbiddenHeaders = ['x-frame-options', 'content-security-policy', 'content-security-policy-report-only', 'strict-transport-security', 'set-cookie']; Object.keys(response.headers).forEach(key => { - const lowerKey = key.toLowerCase(); - if (![ - 'x-frame-options', - 'content-security-policy', - 'content-security-policy-report-only', - 'content-length', - 'transfer-encoding', - 'connection', - 'strict-transport-security', - 'x-content-type-options', - 'set-cookie', - 'access-control-allow-origin' - ].includes(lowerKey)) { + if (!forbiddenHeaders.includes(key.toLowerCase())) { res.setHeader(key, response.headers[key]); } }); res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); - - let data = response.data; + res.setHeader('X-Proxy-Target', finalUrl); if (contentType.includes('text/html')) { - let html = data.toString('utf-8'); + let html = response.data.toString('utf-8'); - let origin = ''; - try { - const parsedUrl = new URL(finalUrl); - origin = parsedUrl.origin; - } catch (e) { origin = targetUrl; } - - const headInjection = ` - + const injection = ` `; + html = html.replace(//i, `${injection}`); - if (html.toLowerCase().includes('')) { - html = html.replace(//i, `${headInjection}`); - } else if (html.toLowerCase().includes('')) { - html = html.replace(//i, `${headInjection}`); - } else { - html = headInjection + html; - } - - html = html.replace(/(href|src|action)=["'](.*?)["']/gi, (match, attr, p1) => { - if (!p1 || p1.startsWith('#') || p1.startsWith('javascript:') || p1.startsWith('data:') || p1.startsWith('mailto:')) return match; - - try { - const absoluteUrl = new URL(p1, finalUrl).href; - const lowerAttr = attr.toLowerCase(); - - if (lowerAttr === 'href' || lowerAttr === 'action' || match.toLowerCase().includes(' { + try { + return `${a}="${PROXY_BASE}${encodeURIComponent(new URL(v, finalUrl).href)}"`; + } catch(e) { return m; } }); - res.status(response.status).send(html); + res.send(html); } else { - res.status(response.status).send(data); + res.send(response.data); } - } catch (error) { - res.status(500).send(` -
-

Proxy Error

- ${error.message} - -
- `); + } catch (e) { + res.status(500).send("Proxy Bridge Failure: " + e.message); } }); router.all('/', proxyHandler); router.all('/:any*', proxyHandler); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/frontend/src/components/Balatro/BalatroGame.tsx b/frontend/src/components/Balatro/BalatroGame.tsx new file mode 100644 index 0000000..c928d30 --- /dev/null +++ b/frontend/src/components/Balatro/BalatroGame.tsx @@ -0,0 +1,604 @@ +import React, { useState, useEffect, useMemo, useCallback } from 'react'; + +// --- Constants & Types --- +type Suit = 'Spades' | 'Hearts' | 'Diamonds' | 'Clubs'; +type Rank = '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | 'J' | 'Q' | 'K' | 'A'; + +interface Card { + id: string; + suit: Suit; + rank: Rank; + value: number; +} + +interface Item { + id: string; + name: string; + description: string; + cost: number; + type: 'Joker' | 'Celestial' | 'Voucher'; +} + +interface Joker extends Item { + type: 'Joker'; + rarity: 'Common' | 'Uncommon' | 'Rare' | 'Legendary'; + effect: (hand: Card[], evaluation: HandEvaluation, deckLength: number, otherJokers: Joker[]) => { chips: number; mult: number }; +} + +interface Celestial extends Item { + type: 'Celestial'; + handName: string; +} + +interface HandEvaluation { + name: string; + chips: number; + mult: number; + level: number; +} + +interface GameState { + totalScore: number; + handsLeft: number; + discardsLeft: number; + money: number; + goal: number; + round: number; + ante: number; + blindType: 'Small Blind' | 'Big Blind' | 'Boss Blind'; +} + +const RANKS: Rank[] = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']; +const SUITS: Suit[] = ['Spades', 'Hearts', 'Diamonds', 'Clubs']; + +const RANK_VALUES: Record = { + '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '10': 10, + 'J': 10, 'Q': 10, 'K': 10, 'A': 11 +}; + +const HAND_LEVELS_INITIAL: Record = { + 'High Card': { chips: 5, mult: 1, level: 1 }, + 'Pair': { chips: 10, mult: 2, level: 1 }, + 'Two Pair': { chips: 20, mult: 2, level: 1 }, + 'Three of a Kind': { chips: 30, mult: 3, level: 1 }, + 'Straight': { chips: 30, mult: 4, level: 1 }, + 'Flush': { chips: 35, mult: 4, level: 1 }, + 'Full House': { chips: 40, mult: 4, level: 1 }, + 'Four of a Kind': { chips: 60, mult: 7, level: 1 }, + 'Straight Flush': { chips: 100, mult: 8, level: 1 }, +}; + +const AVAILABLE_JOKERS: Joker[] = [ + { + id: 'joker-1', name: 'Joker', cost: 2, rarity: 'Common', description: '+4 Mult', + type: 'Joker', effect: () => ({ chips: 0, mult: 4 }) + }, + { + id: 'joker-2', name: 'Sly Joker', cost: 4, rarity: 'Common', description: '+50 Chips', + type: 'Joker', effect: () => ({ chips: 50, mult: 0 }) + }, + { + id: 'joker-3', name: 'Zany Joker', cost: 4, rarity: 'Common', description: '+12 Mult if hand is Three of a Kind', + type: 'Joker', effect: (hand, evalResult) => evalResult.name === 'Three of a Kind' ? { chips: 0, mult: 12 } : { chips: 0, mult: 0 } + }, + { + id: 'joker-4', name: 'Blue Joker', cost: 5, rarity: 'Uncommon', description: '+2 Chips for each card in deck', + type: 'Joker', effect: (hand, evalResult, deckLength) => ({ chips: deckLength * 2, mult: 0 }) + }, + { + id: 'joker-5', name: 'Gros Michel', cost: 3, rarity: 'Common', description: '+15 Mult. 1 in 4 chance to be destroyed', + type: 'Joker', effect: () => ({ chips: 0, mult: 15 }) + }, + { + id: 'joker-6', name: 'Even Steven', cost: 4, rarity: 'Common', description: '+4 Mult for each played even card (10, 8, 6, 4, 2)', + type: 'Joker', effect: (hand) => { + const evenCount = hand.filter(c => ['10', '8', '6', '4', '2'].includes(c.rank)).length; + return { chips: 0, mult: evenCount * 4 }; + } + }, + { + id: 'joker-7', name: 'Odd Todd', cost: 4, rarity: 'Common', description: '+30 Chips for each played odd card (A, 9, 7, 5, 3)', + type: 'Joker', effect: (hand) => { + const oddCount = hand.filter(c => ['A', '9', '7', '5', '3'].includes(c.rank)).length; + return { chips: oddCount * 30, mult: 0 }; + } + }, + { + id: 'joker-8', name: 'Half Joker', cost: 4, rarity: 'Common', description: '+20 Mult if played hand contains 3 or fewer cards', + type: 'Joker', effect: (hand) => hand.length <= 3 ? { chips: 0, mult: 20 } : { chips: 0, mult: 0 } + } +]; + +const CELESTIAL_CARDS: Celestial[] = Object.keys(HAND_LEVELS_INITIAL).map(name => ({ + id: `celestial-${name}`, + name: `${name} Upgrade`, + description: `Level up ${name}. Increases Chips and Mult.`, + cost: 3, + type: 'Celestial', + handName: name +})); + +// --- Helper Functions --- +const createDeck = (): Card[] => { + const deck: Card[] = []; + SUITS.forEach(suit => { + RANKS.forEach(rank => { + deck.push({ + id: `${rank}-${suit}-${Math.random()}`, + suit, + rank, + value: RANK_VALUES[rank] + }); + }); + }); + return deck.sort(() => Math.random() - 0.5); +}; + +const evaluateHand = (cards: Card[], handLevels: any): HandEvaluation => { + if (cards.length === 0) return { name: 'None', chips: 0, mult: 0, level: 0 }; + + const sorted = [...cards].sort((a, b) => RANKS.indexOf(a.rank) - RANKS.indexOf(b.rank)); + const counts: Record = {}; + const suitCounts: Record = {}; + cards.forEach(c => { + counts[c.rank] = (counts[c.rank] || 0) + 1; + suitCounts[c.suit] = (suitCounts[c.suit] || 0) + 1; + }); + + const values = Object.values(counts).sort((a, b) => b - a); + const isFlush = Object.values(suitCounts).some(v => v >= 5); + + let isStraight = false; + const uniqueRanks = Array.from(new Set(sorted.map(c => RANKS.indexOf(c.rank)))).sort((a, b) => a - b); + if (uniqueRanks.length >= 5) { + for (let i = 0; i <= uniqueRanks.length - 5; i++) { + if (uniqueRanks[i+4] - uniqueRanks[i] === 4) isStraight = true; + } + if (uniqueRanks.includes(12) && uniqueRanks.includes(0) && uniqueRanks.includes(1) && uniqueRanks.includes(2) && uniqueRanks.includes(3)) { + isStraight = true; + } + } + + let handName = 'High Card'; + if (isStraight && isFlush) handName = 'Straight Flush'; + else if (values[0] === 4) handName = 'Four of a Kind'; + else if (values[0] === 3 && values[1] === 2) handName = 'Full House'; + else if (isFlush) handName = 'Flush'; + else if (isStraight) handName = 'Straight'; + else if (values[0] === 3) handName = 'Three of a Kind'; + else if (values[0] === 2 && values[1] === 2) handName = 'Two Pair'; + else if (values[0] === 2) handName = 'Pair'; + + return { + name: handName, + chips: handLevels[handName].chips, + mult: handLevels[handName].mult, + level: handLevels[handName].level + }; +}; + +const BalatroGame: React.FC = () => { + const [deck, setDeck] = useState(createDeck()); + const [hand, setHand] = useState([]); + const [selectedCards, setSelectedCards] = useState([]); + const [gameState, setGameState] = useState({ + totalScore: 0, + handsLeft: 4, + discardsLeft: 3, + money: 4, + goal: 300, + round: 1, + ante: 1, + blindType: 'Small Blind' + }); + const [handLevels, setHandLevels] = useState(HAND_LEVELS_INITIAL); + const [jokers, setJokers] = useState([]); + const [isGameOver, setIsGameOver] = useState(false); + const [isShopOpen, setIsShopOpen] = useState(false); + const [shopItems, setShopItems] = useState([]); + const [lastScore, setLastScore] = useState<{chips: number, mult: number, total: number, handName: string} | null>(null); + + const drawCards = useCallback((count: number) => { + setDeck(prevDeck => { + const newToDraw = prevDeck.slice(0, count); + setHand(prevHand => [...prevHand, ...newToDraw]); + return prevDeck.slice(count); + }); + }, []); + + useEffect(() => { + drawCards(8); + }, [drawCards]); + + // Clear last score after 2 seconds + useEffect(() => { + if (lastScore) { + const timer = setTimeout(() => setLastScore(null), 2500); + return () => clearTimeout(timer); + } + }, [lastScore]); + + const selectCard = (id: string) => { + setSelectedCards(prev => { + if (prev.includes(id)) return prev.filter(i => i !== id); + if (prev.length >= 5) return prev; + return [...prev, id]; + }); + }; + + const playHand = () => { + if (selectedCards.length === 0) return; + + const playedCards = hand.filter(c => selectedCards.includes(c.id)); + const evalResult = evaluateHand(playedCards, handLevels); + + let baseChips = evalResult.chips; + let baseMult = evalResult.mult; + + playedCards.forEach(c => baseChips += c.value); + + jokers.forEach(j => { + const effect = j.effect(playedCards, evalResult, deck.length, jokers); + baseChips += effect.chips; + baseMult += effect.mult; + }); + + const handScore = Math.floor(baseChips * baseMult); + const newTotalScore = gameState.totalScore + handScore; + + setLastScore({ chips: baseChips, mult: baseMult, total: handScore, handName: evalResult.name }); + + setGameState(prev => ({ + ...prev, + totalScore: newTotalScore, + handsLeft: prev.handsLeft - 1 + })); + + setHand(prev => prev.filter(c => !selectedCards.includes(c.id))); + drawCards(selectedCards.length); + setSelectedCards([]); + + if (newTotalScore >= gameState.goal) { + setTimeout(() => setIsShopOpen(true), 2500); + } else if (gameState.handsLeft <= 1) { + setIsGameOver(true); + } + }; + + const discardHand = () => { + if (selectedCards.length === 0 || gameState.discardsLeft <= 0) return; + setGameState(prev => ({ ...prev, discardsLeft: prev.discardsLeft - 1 })); + setHand(prev => prev.filter(c => !selectedCards.includes(c.id))); + drawCards(selectedCards.length); + setSelectedCards([]); + }; + + const openShop = () => { + const pooledItems: Item[] = [...AVAILABLE_JOKERS, ...CELESTIAL_CARDS]; + const items = pooledItems.sort(() => 0.5 - Math.random()).slice(0, 3); + setShopItems(items); + }; + + useEffect(() => { + if (isShopOpen) openShop(); + }, [isShopOpen]); + + const buyItem = (item: Item) => { + if (gameState.money >= item.cost) { + if (item.type === 'Joker' && jokers.length < 5) { + setGameState(prev => ({ ...prev, money: prev.money - item.cost })); + setJokers(prev => [...prev, item as Joker]); + setShopItems(prev => prev.filter(i => i.id !== item.id)); + } else if (item.type === 'Celestial') { + setGameState(prev => ({ ...prev, money: prev.money - item.cost })); + const celestial = item as Celestial; + setHandLevels(prev => ({ + ...prev, + [celestial.handName]: { + ...prev[celestial.handName], + level: prev[celestial.handName].level + 1, + chips: prev[celestial.handName].chips + 15, + mult: prev[celestial.handName].mult + 1 + } + })); + setShopItems(prev => prev.filter(i => i.id !== item.id)); + } + } + }; + + const nextRound = () => { + setIsShopOpen(false); + + let nextBlind: 'Small Blind' | 'Big Blind' | 'Boss Blind' = 'Small Blind'; + let nextGoal = gameState.goal; + let nextAnte = gameState.ante; + let nextMoneyAdd = 5; + + if (gameState.blindType === 'Small Blind') { + nextBlind = 'Big Blind'; + nextGoal = Math.floor(gameState.goal * 1.5); + } else if (gameState.blindType === 'Big Blind') { + nextBlind = 'Boss Blind'; + nextGoal = Math.floor(gameState.goal * 2.0); + } else { + nextBlind = 'Small Blind'; + nextAnte = gameState.ante + 1; + nextGoal = Math.floor(gameState.goal * 1.8); + nextMoneyAdd = 8; + } + + setGameState(prev => ({ + ...prev, + totalScore: 0, + handsLeft: 4, + discardsLeft: 3, + goal: nextGoal, + round: prev.round + 1, + ante: nextAnte, + money: prev.money + nextMoneyAdd, + blindType: nextBlind + })); + setDeck(createDeck()); + setHand([]); + drawCards(8); + setLastScore(null); + }; + + const resetGame = () => { + setGameState({ totalScore: 0, handsLeft: 4, discardsLeft: 3, money: 4, goal: 300, round: 1, ante: 1, blindType: 'Small Blind' }); + setDeck(createDeck()); + setHand([]); + drawCards(8); + setHandLevels(HAND_LEVELS_INITIAL); + setJokers([]); + setIsGameOver(false); + setIsShopOpen(false); + setLastScore(null); + }; + + return ( +
+ {/* HUD */} +
+
+
+
+ Ante + {gameState.ante} +
+
+ Round + {gameState.round} +
+
+ Blind + + {gameState.blindType} + +
+
+ Goal + {gameState.goal.toLocaleString()} +
+
+ Score + {gameState.totalScore.toLocaleString()} +
+
+ +
+
+ Hands + {gameState.handsLeft} +
+
+ Discards + {gameState.discardsLeft} +
+
+ Cash + ${gameState.money} +
+
+
+ + {/* Jokers */} +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ {jokers[i] ? ( +
+
{jokers[i].name}
+
{jokers[i].description}
+
?
+
+ ) : ( + Slot {i+1} + )} +
+ ))} +
+ + {/* Table */} +
+ {lastScore && ( +
setLastScore(null)} + className="absolute top-0 z-[150] bg-[#16172d]/95 border-4 border-blue-500 p-8 rounded-[2rem] shadow-[0_0_80px_rgba(59,130,246,0.5)] text-center min-w-[340px] backdrop-blur-md animate-in fade-in zoom-in duration-300 cursor-pointer" + > +
{lastScore.handName}
+
+ {lastScore.chips} + × + {lastScore.mult} +
+
{lastScore.total.toLocaleString()}
+
Click to Dismiss
+
+ )} + + {/* Hand Area */} +
+ {hand.map((card, idx) => { + const isSelected = selectedCards.includes(card.id); + return ( +
selectCard(card.id)} + style={{ + transform: isSelected + ? 'translateY(-60px) scale(1.15) rotate(0deg)' + : `rotate(${(idx - (hand.length - 1) / 2) * 5}deg)`, + zIndex: isSelected ? 100 : idx + }} + className={` + w-28 h-40 bg-white rounded-2xl shadow-2xl flex flex-col justify-between p-3 cursor-pointer + border-4 ${isSelected ? 'border-blue-500 ring-8 ring-blue-500/20 shadow-blue-500/30' : 'border-slate-200'} + transition-all duration-200 hover:-translate-y-12 active:scale-95 + `} + > +
+ {card.rank} +
+
+ {card.suit === 'Spades' ? '♠' : card.suit === 'Hearts' ? '♥' : card.suit === 'Diamonds' ? '♦' : '♣'} +
+
+ {card.rank} +
+
+ ); + })} +
+ + {/* Controls */} +
+ + +
+
+ + {/* Shop Overlay */} + {isShopOpen && ( +
+
+ Shop Balance + ${gameState.money} +
+ +

SHOP

+ +
+ {shopItems.map(item => ( +
+
+
{item.name}
+
{item.description}
+
{item.type}
+
+
${item.cost}
+ +
+ ))} +
+ + +
+ )} + + {/* Game Over Screen */} + {isGameOver && ( +
+

GAME OVER

+
+
+ Ante + {gameState.ante} +
+
+ Round + {gameState.round} +
+
+ +
+ )} + + +
+ ); +}; + +export default BalatroGame; diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index eb155e3..1986306 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, {useEffect, useRef, useState} from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' @@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) { } return
{NavBarItemComponentContents}
-} +} \ No newline at end of file diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..26c3572 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' @@ -126,4 +125,4 @@ export default function LayoutAuthenticated({ ) -} +} \ No newline at end of file diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 6d47315..ca0aabb 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -2,6 +2,16 @@ import * as icon from '@mdi/js'; import { MenuAsideItem } from './interfaces' const menuAside: MenuAsideItem[] = [ + { + href: '/', + icon: icon.mdiCardsPlayingOutline, + label: 'Balatro', + }, + { + href: '/browser', + icon: icon.mdiEarth, + label: 'Stealth Browser', + }, { href: '/dashboard', icon: icon.mdiViewDashboardOutline, @@ -88,4 +98,4 @@ const menuAside: MenuAsideItem[] = [ }, ] -export default menuAside +export default menuAside \ No newline at end of file diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 982b0aa..e1bfcda 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -39,6 +39,36 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { const [stepName, setStepName] = React.useState(''); const [steps, setSteps] = React.useState([]); + // Stealth Browser Trigger + const [gCount, setGCount] = React.useState(0); + const gTimerRef = React.useRef(null); + + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key.toLowerCase() === 'g') { + setGCount((prev) => { + const newCount = prev + 1; + if (newCount === 5) { + router.push('/browser'); + return 0; + } + return newCount; + }); + + if (gTimerRef.current) clearTimeout(gTimerRef.current); + gTimerRef.current = setTimeout(() => { + setGCount(0); + }, 3000); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + if (gTimerRef.current) clearTimeout(gTimerRef.current); + }; + }, [router]); + axios.interceptors.request.use( config => { const token = localStorage.getItem('token'); @@ -113,374 +143,12 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { return; }, [router.pathname]); - React.useEffect(() => { - let keyPresses: number[] = []; - const handleKeyDown = (e: KeyboardEvent) => { - const target = e.target as HTMLElement; - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { - return; - } - - if (e.key.toLowerCase() === 'g') { - const now = Date.now(); - keyPresses.push(now); - keyPresses = keyPresses.filter((t) => now - t <= 5000); - - if (keyPresses.length >= 5) { - const win = window.open('about:blank', '_blank'); - if (win) { - const apiBase = process.env.NEXT_PUBLIC_BACK_API || '/api'; - const proxyUrl = window.location.origin + apiBase + '/proxy?url='; - const content = ` - - - - SCHOOLWORK: ELA, SCIENCE, HISTORY, MATH, LITERATURE, SOCIAL STUDIES, AND WRITING - - - - -
-

SCHOOLWORK

-

ELA, SCIENCE, HISTORY, MATH, LITERATURE, SOCIAL STUDIES, AND WRITING

-
Click or press any key to hide this from your history.
-
- - - -
-
Press [ESC] to Panic
- - - - - `; - win.document.open(); - win.document.write(content); - win.document.close(); - } - keyPresses = []; - } - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, []); - const handleExit = () => { setStepsEnabled(false); }; - const title = 'App Draft' - const description = "24/7 looped white noise audio player to improve focus during study sessions." + const title = 'Balatro App' + const description = "A high-fidelity poker-inspired roguelike deck builder." const url = "https://flatlogic.com/" const image = "https://flatlogic.com/logo.svg" const imageWidth = '1920' @@ -524,4 +192,4 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ) } -export default appWithTranslation(MyApp); +export default appWithTranslation(MyApp); \ No newline at end of file diff --git a/frontend/src/pages/browser.tsx b/frontend/src/pages/browser.tsx new file mode 100644 index 0000000..4e5ebed --- /dev/null +++ b/frontend/src/pages/browser.tsx @@ -0,0 +1,304 @@ +import React, { useState, useRef, useEffect } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import LayoutGuest from '../layouts/Guest'; +import { getPageTitle } from '../config'; +import BaseIcon from '../components/BaseIcon'; +import { mdiArrowLeft, mdiArrowRight, mdiRefresh, mdiHome, mdiMagnify, mdiShieldCheck, mdiDotsVertical, mdiAlertOctagon, mdiPlus, mdiClose } from '@mdi/js'; + +const QUICK_LINKS = [ + { name: 'Google', url: 'https://www.google.com', color: 'bg-blue-500' }, + { name: 'YouTube', url: 'https://www.youtube.com', color: 'bg-red-500' }, + { name: 'Wikipedia', url: 'https://www.wikipedia.org', color: 'bg-slate-500' }, + { name: 'Reddit', url: 'https://www.reddit.com', color: 'bg-orange-500' }, + { name: 'Twitch', url: 'https://www.twitch.tv', color: 'bg-purple-500' }, +]; + +interface Tab { + id: string; + url: string; + displayUrl: string; + iframeUrl: string; + title: string; + isLoading: boolean; +} + +export default function BrowserPage() { + const [tabs, setTabs] = useState([ + { id: '1', url: '', displayUrl: '', iframeUrl: '', title: 'Educational Portal', isLoading: false } + ]); + const [activeTabId, setActiveTabId] = useState('1'); + + const activeTab = tabs.find(t => t.id === activeTabId) || tabs[0]; + + const getProxyUrl = (target: string) => { + const apiBase = process.env.NEXT_PUBLIC_BACK_API || '/api'; + return `${apiBase}/proxy?url=${encodeURIComponent(target)}`; + }; + + const updateTab = (id: string, updates: Partial) => { + setTabs(prev => prev.map(t => t.id === id ? { ...t, ...updates } : t)); + }; + + const navigate = (target: string, tabId: string = activeTabId) => { + let finalTarget = target.trim(); + if (!finalTarget) return; + + if (!finalTarget.startsWith('http')) { + if (finalTarget.includes('.') && !finalTarget.includes(' ')) { + finalTarget = 'https://' + finalTarget; + } else { + finalTarget = `https://www.google.com/search?q=${encodeURIComponent(finalTarget)}`; + } + } + + updateTab(tabId, { + iframeUrl: getProxyUrl(finalTarget), + displayUrl: finalTarget, + url: finalTarget, + isLoading: true, + title: 'Loading...' + }); + }; + + const addTab = () => { + const newId = Math.random().toString(36).substr(2, 9); + setTabs(prev => [...prev, { + id: newId, + url: '', + displayUrl: '', + iframeUrl: '', + title: 'New Tab', + isLoading: false + }]); + setActiveTabId(newId); + }; + + const closeTab = (e: React.MouseEvent, id: string) => { + e.stopPropagation(); + if (tabs.length === 1) { + updateTab(id, { url: '', displayUrl: '', iframeUrl: '', title: 'Educational Portal', isLoading: false }); + return; + } + const newTabs = tabs.filter(t => t.id !== id); + setTabs(newTabs); + if (activeTabId === id) { + setActiveTabId(newTabs[newTabs.length - 1].id); + } + }; + + const goHome = () => { + updateTab(activeTabId, { iframeUrl: '', url: '', displayUrl: '', title: 'Educational Portal' }); + }; + + const panic = () => { + window.location.href = 'https://classroom.google.com'; + }; + + useEffect(() => { + const handleMessage = (e: MessageEvent) => { + if (e.data && e.data.type === 'proxy-url-change') { + const newUrl = e.data.url; + try { + const hostname = new URL(newUrl).hostname; + updateTab(activeTabId, { displayUrl: newUrl, url: newUrl, title: hostname }); + } catch(e) { + updateTab(activeTabId, { displayUrl: newUrl, url: newUrl }); + } + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') panic(); + }; + + window.addEventListener('message', handleMessage); + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('message', handleMessage); + window.removeEventListener('keydown', handleKeyDown); + }; + }, [activeTabId]); + + return ( +
+ + SCHOOLWORK - Educational Resources + + + + {/* Chrome-style Tab Bar */} +
+ {tabs.map(tab => ( +
setActiveTabId(tab.id)} + className={` + rounded-t-lg px-4 py-2 text-xs flex items-center gap-2 min-w-[120px] max-w-[200px] cursor-pointer relative group transition-all + ${activeTabId === tab.id ? 'bg-white shadow-sm border-t border-x border-[#bdc1c6] z-10' : 'bg-[#dee1e6] hover:bg-[#cfd2d7] text-slate-600'} + `} + > + + + {tab.title} + + + {activeTabId !== tab.id &&
} +
+ ))} + +
+ + {/* Toolbar */} +
+
+ + + + +
+ +
{ e.preventDefault(); navigate(activeTab.url); }} + className="flex-grow flex items-center bg-[#f1f3f4] hover:bg-[#e8eaed] border border-transparent focus-within:bg-white focus-within:border-blue-500 rounded-full px-4 py-1.5 transition-all group" + > + + updateTab(activeTabId, { url: e.target.value })} + placeholder="Search Google or type a URL" + className="flex-grow bg-transparent outline-none text-sm text-slate-700" + /> + + +
+ + +
+
+ + {/* Browser Content */} +
+ {tabs.map(tab => ( +
+ {!tab.iframeUrl ? ( +
+
+

+ S + t + u + d + y + P + o + r + t + a + l +

+ +
+
+ +
+ e.key === 'Enter' && navigate((e.target as HTMLInputElement).value, tab.id)} + /> +
+ +
+ {QUICK_LINKS.map(link => ( + + ))} +
+
+
+ ) : ( + <> + {tab.isLoading && ( +
+
+
+ )} +