Autosave: 20260203-021911

This commit is contained in:
Flatlogic Bot 2026-02-03 02:19:12 +00:00
parent 3efcff95c6
commit b4c212d6cc
8 changed files with 1110 additions and 929 deletions

View File

@ -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(`
<div style="background:#1a1a1a; color:#ccc; padding:20px; font-family:sans-serif; border-radius:8px; margin:20px; border:1px solid #333;">
<h2 style="color:#ff4d4d; margin-top:0;">URL Required</h2>
<p>The stealth proxy needs a target URL to work.</p>
<button onclick="window.history.back()" style="background:#333; color:#fff; border:none; padding:8px 16px; border-radius:4px; cursor:pointer; margin-top:15px;">Go Back</button>
</div>
`);
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 = `
<base href="${origin}/">
const injection = `
<script>
(function() {
const TARGET_URL = "${finalUrl}";
const PROXY_BASE = window.location.origin + "${PROXY_PATH}?url=";
function wrapUrl(u) {
if (!u || typeof u !== 'string' || u.startsWith('data:') || u.startsWith('javascript:') || u.startsWith('blob:')) return u;
if (u.startsWith(window.location.origin)) return u;
try {
const abs = new URL(u, TARGET_URL).href;
return PROXY_BASE + encodeURIComponent(abs);
} catch(e) { return u; }
}
// --- Network Virtualization ---
const _fetch = window.fetch;
window.fetch = (input, init) => {
if (typeof input === 'string') input = wrapUrl(input);
else if (input instanceof Request) {
Object.defineProperty(input, 'url', { value: wrapUrl(input.url) });
}
return _fetch(input, init);
};
const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
return _open.apply(this, [method, wrapUrl(url), async, user, password]);
};
// --- Location & History Spoofing ---
const targetOrigin = new URL(TARGET_URL).origin;
const locationProxy = new Proxy(window.location, {
get: (t, p) => {
if (p === 'href') return TARGET_URL;
if (p === 'origin') return targetOrigin;
if (p === 'host' || p === 'hostname') return new URL(TARGET_URL).hostname;
const val = t[p];
return typeof val === 'function' ? val.bind(t) : val;
},
set: (t, p, v) => {
if (p === 'href') window.location.href = wrapUrl(v);
else t[p] = v;
return true;
}
});
try {
var PROXY_BASE = window.location.origin + "/api/proxy?url=";
var TARGET_ORIGIN = "${origin}";
var FINAL_URL = "${finalUrl}";
function toProxyUrl(url) {
if (!url || typeof url !== 'string') return url;
if (url.startsWith('javascript:') || url.startsWith('data:') || url.startsWith('mailto:') || url.startsWith('tel:')) return url;
if (url.startsWith(window.location.origin + "/api/proxy")) return url;
try {
var absUrl = new URL(url, FINAL_URL).href;
return PROXY_BASE + encodeURIComponent(absUrl);
} catch(e) { return url; }
}
Object.defineProperty(window, 'location', { value: locationProxy, configurable: true });
Object.defineProperty(document, 'location', { value: locationProxy, configurable: true });
} catch(e) {}
function notifyParent() {
try {
window.parent.postMessage({
type: 'zen-browser-url-change',
url: window.location.href,
title: document.title
}, '*');
} catch(e) {}
}
const _pushState = history.pushState;
history.pushState = (state, title, url) => {
if (url) window.parent.postMessage({ type: 'proxy-url-change', url: new URL(url, TARGET_URL).href }, '*');
return _pushState.apply(history, [state, title, url ? wrapUrl(url) : url]);
};
var originalFetch = window.fetch;
window.fetch = function(input, init) {
if (typeof input === 'string') {
input = toProxyUrl(input);
} else if (input instanceof Request) {
input = new Request(toProxyUrl(input.url), input);
const _replaceState = history.replaceState;
history.replaceState = (state, title, url) => {
if (url) window.parent.postMessage({ type: 'proxy-url-change', url: new URL(url, TARGET_URL).href }, '*');
return _replaceState.apply(history, [state, title, url ? wrapUrl(url) : url]);
};
// --- Attribute Rewriting (Real-time) ---
const observer = new MutationObserver(mutations => {
document.querySelectorAll('a:not([data-proxied]), form:not([data-proxied])').forEach(el => {
const attr = el.tagName === 'A' ? 'href' : 'action';
const val = el.getAttribute(attr);
if (val && !val.startsWith('#') && !val.startsWith('javascript:')) {
el.setAttribute(attr, wrapUrl(val));
el.setAttribute('data-proxied', 'true');
}
return originalFetch(input, init);
};
var originalOpenXHR = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
return originalOpenXHR.apply(this, [method, toProxyUrl(url), true]);
};
if (navigator.sendBeacon) {
var originalSendBeacon = navigator.sendBeacon;
navigator.sendBeacon = function(url, data) {
return originalSendBeacon.apply(this, [toProxyUrl(url), data]);
};
}
var originalPushState = history.pushState;
history.pushState = function(state, title, url) {
var res = originalPushState.apply(this, arguments);
notifyParent();
return res;
};
var originalReplaceState = history.replaceState;
history.replaceState = function() {
var res = originalReplaceState.apply(this, arguments);
notifyParent();
return res;
};
window.addEventListener('popstate', notifyParent);
var originalOpen = window.open;
window.open = function(url, name, specs) {
if (!url) return originalOpen(url, name, specs);
window.location.href = toProxyUrl(url);
return null;
};
Object.defineProperty(window, 'top', { get: function() { return window.self; } });
Object.defineProperty(window, 'parent', { get: function() { return window.self; } });
var originalReplace = window.location.replace;
window.location.replace = function(url) {
window.location.href = toProxyUrl(url);
};
window.addEventListener('submit', function(e) {
var form = e.target;
if (form.method.toLowerCase() === 'get') {
// For GET forms, we want to stay in the proxy.
// If we change the action to /api/proxy?url=target, the browser will append ?field=val
// resulting in /api/proxy?field=val (losing the url parameter).
// So we add a hidden input for the 'url'.
var action = form.getAttribute('action') || '';
var absoluteAction = new URL(action, FINAL_URL).href;
// Clean up existing url inputs
form.querySelectorAll('input[name="url"]').forEach(el => el.remove());
var urlInput = document.createElement('input');
urlInput.type = 'hidden';
urlInput.name = 'url';
urlInput.value = absoluteAction;
form.appendChild(urlInput);
form.action = window.location.origin + "/api/proxy";
} else if (form.action && !form.action.includes("/api/proxy")) {
form.action = toProxyUrl(form.action);
});
document.querySelectorAll('img:not([data-proxied]), script:not([data-proxied]), link:not([data-proxied])').forEach(el => {
const attr = el.tagName === 'LINK' ? 'href' : 'src';
const val = el.getAttribute(attr);
if (val && !val.startsWith('data:') && !val.startsWith('blob:')) {
el.setAttribute(attr, wrapUrl(val));
el.setAttribute('data-proxied', 'true');
}
}, true);
});
});
observer.observe(document.documentElement, { childList: true, subtree: true });
setInterval(function() {
document.querySelectorAll('a[href]:not([data-proxied]), form[action]:not([data-proxied])').forEach(function(el) {
var attr = el.tagName === 'A' ? 'href' : 'action';
var val = el.getAttribute(attr);
if (val && !val.includes('/api/proxy') && !val.startsWith('#') && !val.startsWith('javascript:')) {
el.setAttribute(attr, toProxyUrl(val));
el.setAttribute('data-proxied', 'true');
}
});
}, 1000);
// --- Form Interception ---
window.addEventListener('submit', e => {
const form = e.target;
if (form.method.toLowerCase() === 'get') {
e.preventDefault();
const action = new URL(form.getAttribute('action') || '', TARGET_URL).href;
const params = new URLSearchParams(new FormData(form)).toString();
window.location.href = wrapUrl(action + (action.includes('?') ? '&' : '?') + params);
}
}, true);
notifyParent();
} catch(e) { console.error("Proxy injection error:", e); }
})();
</script>
`;
html = html.replace(/<head>/i, `<head>${injection}`);
if (html.toLowerCase().includes('<head>')) {
html = html.replace(/<head>/i, `<head>${headInjection}`);
} else if (html.toLowerCase().includes('<html>')) {
html = html.replace(/<html>/i, `<html><head>${headInjection}</head>`);
} 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('<script') || match.toLowerCase().includes('<iframe')) {
if (p1.includes('/api/proxy')) return match;
return `${attr}="/api/proxy?url=${encodeURIComponent(absoluteUrl)}"`;
}
return match;
} catch(e) { return match; }
// Initial static rewrite
html = html.replace(/(href|src|action)=["']((?!data:|javascript:|#|blob:).*?)["']/gi, (m, a, v) => {
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(`
<div style="background:#1a1a1a; color:#ff4d4d; padding:20px; font-family:sans-serif; border-radius:8px; margin:20px; border:1px solid #333;">
<h2>Proxy Error</h2>
<code>${error.message}</code>
<button onclick="window.location.reload()" style="margin-top:15px; background:#3ea6ff; color:#fff; border:none; padding:8px 16px; border-radius:4px; cursor:pointer;">Retry</button>
</div>
`);
} catch (e) {
res.status(500).send("Proxy Bridge Failure: " + e.message);
}
});
router.all('/', proxyHandler);
router.all('/:any*', proxyHandler);
module.exports = router;
module.exports = router;

View File

@ -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<Rank, number> = {
'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<string, { chips: number, mult: number, level: number }> = {
'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<string, number> = {};
const suitCounts: Record<string, number> = {};
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<Card[]>(createDeck());
const [hand, setHand] = useState<Card[]>([]);
const [selectedCards, setSelectedCards] = useState<string[]>([]);
const [gameState, setGameState] = useState<GameState>({
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<Joker[]>([]);
const [isGameOver, setIsGameOver] = useState(false);
const [isShopOpen, setIsShopOpen] = useState(false);
const [shopItems, setShopItems] = useState<Item[]>([]);
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 (
<div className="min-h-screen bg-[#0d0e1b] text-white p-6 font-mono overflow-hidden select-none">
{/* HUD */}
<div className="flex gap-4 mb-6">
<div className="flex-grow flex gap-6 bg-[#16172d] p-4 rounded-2xl border border-[#2b2c4e] shadow-2xl relative overflow-hidden group">
<div className="absolute inset-0 bg-blue-500/5 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div className="flex flex-col">
<span className="text-[10px] text-slate-500 font-black uppercase tracking-widest">Ante</span>
<span className="text-2xl text-purple-400 font-bold">{gameState.ante}</span>
</div>
<div className="flex flex-col border-l border-slate-700 pl-6">
<span className="text-[10px] text-slate-500 font-black uppercase tracking-widest">Round</span>
<span className="text-2xl text-blue-400 font-bold">{gameState.round}</span>
</div>
<div className="flex flex-col border-l border-slate-700 pl-6">
<span className="text-[10px] text-slate-500 font-black uppercase tracking-widest">Blind</span>
<span className={`text-xl font-bold uppercase ${gameState.blindType === 'Boss Blind' ? 'text-red-500 animate-pulse' : 'text-orange-400'}`}>
{gameState.blindType}
</span>
</div>
<div className="flex flex-col border-l border-slate-700 pl-6">
<span className="text-[10px] text-slate-500 font-black uppercase tracking-widest">Goal</span>
<span className="text-2xl text-orange-500 font-bold">{gameState.goal.toLocaleString()}</span>
</div>
<div className="flex flex-col border-l border-slate-700 pl-6 flex-grow">
<span className="text-[10px] text-slate-500 font-black uppercase tracking-widest">Score</span>
<span className="text-3xl text-white font-black">{gameState.totalScore.toLocaleString()}</span>
</div>
</div>
<div className="flex gap-3">
<div className="bg-red-600/10 px-6 py-2 rounded-2xl border-2 border-red-600 flex flex-col items-center justify-center min-w-[100px] shadow-[0_0_15px_rgba(220,38,38,0.2)]">
<span className="text-[10px] font-black text-red-500 uppercase">Hands</span>
<span className="text-3xl font-black">{gameState.handsLeft}</span>
</div>
<div className="bg-blue-600/10 px-6 py-2 rounded-2xl border-2 border-blue-600 flex flex-col items-center justify-center min-w-[100px] shadow-[0_0_15px_rgba(37,99,235,0.2)]">
<span className="text-[10px] font-black text-blue-500 uppercase">Discards</span>
<span className="text-3xl font-black">{gameState.discardsLeft}</span>
</div>
<div className="bg-yellow-600/10 px-6 py-2 rounded-2xl border-2 border-yellow-600 flex flex-col items-center justify-center min-w-[100px] shadow-[0_0_15px_rgba(202,138,4,0.2)]">
<span className="text-[10px] font-black text-yellow-500 uppercase">Cash</span>
<span className="text-3xl font-black font-mono">${gameState.money}</span>
</div>
</div>
</div>
{/* Jokers */}
<div className="flex gap-4 mb-12 h-32 justify-center">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="w-24 h-32 rounded-xl border-2 border-dashed border-slate-800 flex items-center justify-center relative bg-slate-900/30">
{jokers[i] ? (
<div
className="w-full h-full bg-gradient-to-br from-indigo-600 to-purple-700 rounded-xl p-3 text-[11px] flex flex-col justify-between border border-indigo-400 shadow-[0_0_20px_rgba(99,102,241,0.5)] transition-all duration-300 transform hover:scale-105 hover:rotate-1 group cursor-help"
>
<div className="font-black italic text-white uppercase leading-none truncate">{jokers[i].name}</div>
<div className="text-[9px] opacity-90 leading-tight font-medium line-clamp-3">{jokers[i].description}</div>
<div className="absolute -top-2 -right-2 w-6 h-6 bg-indigo-500 rounded-full flex items-center justify-center text-[10px] border-2 border-[#16172d] shadow-lg">?</div>
</div>
) : (
<span className="text-slate-800 text-[10px] font-black uppercase tracking-tighter">Slot {i+1}</span>
)}
</div>
))}
</div>
{/* Table */}
<div className="relative flex flex-col items-center justify-center min-h-[480px]">
{lastScore && (
<div
onClick={() => 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"
>
<div className="text-blue-400 text-xl font-black uppercase mb-3 tracking-widest">{lastScore.handName}</div>
<div className="flex items-center justify-center gap-6 text-5xl mb-3">
<span className="text-blue-400 font-black drop-shadow-[0_0_10px_rgba(59,130,246,0.5)]">{lastScore.chips}</span>
<span className="text-slate-600 font-light">×</span>
<span className="text-red-500 font-black drop-shadow-[0_0_10px_rgba(239,68,68,0.5)]">{lastScore.mult}</span>
</div>
<div className="text-6xl font-black text-white tracking-tighter">{lastScore.total.toLocaleString()}</div>
<div className="mt-4 text-[10px] text-slate-500 uppercase font-black">Click to Dismiss</div>
</div>
)}
{/* Hand Area */}
<div className="flex gap-2 justify-center mt-28">
{hand.map((card, idx) => {
const isSelected = selectedCards.includes(card.id);
return (
<div
key={card.id}
onClick={() => 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
`}
>
<div className={`text-2xl font-black leading-none ${['Hearts', 'Diamonds'].includes(card.suit) ? 'text-red-600' : 'text-slate-900'}`}>
{card.rank}
</div>
<div className={`text-6xl self-center ${['Hearts', 'Diamonds'].includes(card.suit) ? 'text-red-600' : 'text-slate-900'}`}>
{card.suit === 'Spades' ? '♠' : card.suit === 'Hearts' ? '♥' : card.suit === 'Diamonds' ? '♦' : '♣'}
</div>
<div className={`text-2xl font-black leading-none rotate-180 self-end ${['Hearts', 'Diamonds'].includes(card.suit) ? 'text-red-600' : 'text-slate-900'}`}>
{card.rank}
</div>
</div>
);
})}
</div>
{/* Controls */}
<div className="mt-24 flex gap-8">
<button
onClick={playHand}
disabled={selectedCards.length === 0 || !!lastScore}
className="px-20 py-7 bg-blue-600 hover:bg-blue-500 disabled:bg-slate-900 disabled:text-slate-700 rounded-[2rem] font-black text-3xl shadow-[0_12px_0_rgb(29,78,216)] active:translate-y-2 active:shadow-none transition-all flex flex-col items-center"
>
PLAY HAND
<span className="text-xs opacity-50 font-medium tracking-widest mt-1">CHIPS × MULT</span>
</button>
<button
onClick={discardHand}
disabled={selectedCards.length === 0 || gameState.discardsLeft <= 0 || !!lastScore}
className="px-20 py-7 bg-red-600 hover:bg-red-500 disabled:bg-slate-900 disabled:text-slate-700 rounded-[2rem] font-black text-3xl shadow-[0_12px_0_rgb(185,28,28)] active:translate-y-2 active:shadow-none transition-all flex flex-col items-center"
>
DISCARD
<span className="text-xs opacity-50 font-medium tracking-widest mt-1">{gameState.discardsLeft} LEFT</span>
</button>
</div>
</div>
{/* Shop Overlay */}
{isShopOpen && (
<div
className="fixed inset-0 bg-[#0d0e1b]/98 backdrop-blur-2xl z-[200] flex flex-col items-center justify-center p-12 animate-in fade-in duration-700"
>
<div className="absolute top-10 left-10 flex flex-col">
<span className="text-slate-500 font-black uppercase tracking-widest text-sm">Shop Balance</span>
<span className="text-6xl font-black text-yellow-500 font-mono">${gameState.money}</span>
</div>
<h2 className="text-9xl font-black text-blue-500 mb-16 italic tracking-tighter drop-shadow-[0_0_40px_rgba(59,130,246,0.6)]">SHOP</h2>
<div className="flex gap-12 mb-24">
{shopItems.map(item => (
<div key={item.id} className="w-72 bg-[#16172d] border-4 border-[#2b2c4e] rounded-[3rem] p-10 flex flex-col items-center text-center group hover:border-blue-500 transition-all hover:-translate-y-4 shadow-[0_30px_60px_rgba(0,0,0,0.5)]">
<div className={`w-48 h-64 rounded-3xl mb-10 shadow-2xl flex flex-col p-6 justify-between border-2 transform group-hover:rotate-2 transition-transform ${
item.type === 'Joker' ? 'bg-gradient-to-br from-indigo-600 to-purple-700 border-indigo-400' : 'bg-gradient-to-br from-pink-600 to-orange-600 border-pink-400'
}`}>
<div className="font-black text-xl italic leading-none uppercase tracking-tighter">{item.name}</div>
<div className="text-xs opacity-90 font-bold leading-tight uppercase tracking-tight">{item.description}</div>
<div className="text-[10px] self-end font-black opacity-50">{item.type}</div>
</div>
<div className="text-yellow-500 font-black text-4xl mb-8 font-mono">${item.cost}</div>
<button
onClick={() => buyItem(item)}
disabled={gameState.money < item.cost || (item.type === 'Joker' && jokers.length >= 5)}
className={`w-full py-5 rounded-2xl font-black uppercase tracking-widest text-sm transition-all shadow-xl ${
gameState.money < item.cost || (item.type === 'Joker' && jokers.length >= 5)
? 'bg-slate-800 text-slate-500 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-500 text-white shadow-blue-900/40 active:scale-95'
}`}
>
{gameState.money < item.cost ? 'NOT ENOUGH' : (item.type === 'Joker' && jokers.length >= 5) ? 'SLOTS FULL' : 'BUY ITEM'}
</button>
</div>
))}
</div>
<button
onClick={nextRound}
className="px-32 py-10 bg-orange-600 hover:bg-orange-500 rounded-[2.5rem] font-black text-5xl shadow-[0_15px_0_rgb(194,65,12)] active:translate-y-2 active:shadow-none transition-all shadow-orange-900/40 group relative"
>
NEXT BLIND
<span className="absolute -top-4 -right-4 bg-white text-orange-600 rounded-full w-12 h-12 flex items-center justify-center text-2xl animate-bounce"></span>
</button>
</div>
)}
{/* Game Over Screen */}
{isGameOver && (
<div className="fixed inset-0 bg-black/95 z-[300] flex flex-col items-center justify-center p-12 animate-in fade-in zoom-in duration-500">
<h2 className="text-[12rem] font-black text-red-600 mb-8 italic tracking-tighter drop-shadow-[0_0_60px_rgba(220,38,38,0.5)]">GAME OVER</h2>
<div className="flex gap-20 text-4xl mb-16 text-slate-400 font-black">
<div className="flex flex-col items-center">
<span className="text-sm opacity-50 uppercase mb-2">Ante</span>
<span className="text-white text-6xl">{gameState.ante}</span>
</div>
<div className="flex flex-col items-center">
<span className="text-sm opacity-50 uppercase mb-2">Round</span>
<span className="text-white text-6xl">{gameState.round}</span>
</div>
</div>
<button
onClick={resetGame}
className="px-28 py-9 bg-white text-black rounded-[2.5rem] font-black text-5xl hover:bg-blue-500 hover:text-white transition-all shadow-[0_15px_0_rgb(203,213,225)] active:translate-y-2 active:shadow-none"
>
NEW RUN
</button>
</div>
)}
<style jsx global>{`
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&display=swap');
body {
background-color: #0d0e1b;
color: white;
font-family: 'Roboto Mono', monospace;
user-select: none;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes zoom-in {
from { transform: scale(0.9); }
to { transform: scale(1); }
}
.animate-in {
animation: fade-in 0.3s ease-out forwards;
}
.fade-in {
animation: fade-in 0.5s ease-out forwards;
}
.zoom-in {
animation: zoom-in 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
`}</style>
</div>
);
};
export default BalatroGame;

View File

@ -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 <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
}
}

View File

@ -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({
</div>
</div>
)
}
}

View File

@ -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

View File

@ -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<NodeJS.Timeout | null>(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 = `
<!DOCTYPE html>
<html>
<head>
<title>SCHOOLWORK: ELA, SCIENCE, HISTORY, MATH, LITERATURE, SOCIAL STUDIES, AND WRITING</title>
<link rel="icon" href="https://www.google.com/favicon.ico">
<style>
body { background: #fff; margin: 0; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden; color: #333; }
/* Splash Screen */
#splash { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #fff; z-index: 9999; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; cursor: pointer; transition: opacity 0.5s; }
#splash h1 { font-size: 24px; color: #1a73e8; margin-bottom: 20px; font-weight: 500; }
#splash p { font-size: 16px; color: #5f6368; }
#splash .hint { margin-top: 50px; font-size: 14px; color: #999; border: 1px solid #eee; padding: 10px 20px; border-radius: 4px; }
/* Main UI */
.header { background: #f8f9fa; border-bottom: 1px solid #dadce0; display: flex; flex-direction: column; z-index: 10; display: none; }
.tab-bar { display: flex; padding: 10px 10px 0; gap: 4px; overflow-x: auto; background: #dee2e6; align-items: flex-end; }
.tab { padding: 8px 16px; background: #e9ecef; border-radius: 8px 8px 0 0; cursor: pointer; display: flex; align-items: center; gap: 10px; font-size: 12px; min-width: 100px; max-width: 180px; white-space: nowrap; overflow: hidden; border: 1px solid #dee2e6; border-bottom: none; transition: all 0.2s; color: #495057; position: relative; }
.tab:hover { background: #f1f3f4; }
.tab.active { background: #fff; color: #202124; font-weight: 500; border-color: #dadce0; border-bottom-color: #fff; margin-bottom: -1px; z-index: 1; border-top: 3px solid #1a73e8; }
.tab .close { font-size: 16px; color: #70757a; width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; border-radius: 50%; }
.tab .close:hover { background: #dadce0; color: #202124; }
.search-bar { padding: 8px 12px; display: flex; gap: 8px; align-items: center; background: #fff; border-bottom: 1px solid #eee; }
.nav-btns { display: flex; gap: 4px; }
.nav-btn { cursor: pointer; color: #5f6368; padding: 6px; border-radius: 50%; display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; transition: 0.2s; }
.nav-btn:hover { background: #f1f3f4; color: #202124; }
.nav-btn.disabled { color: #bdc1c6; cursor: default; pointer-events: none; }
form { flex-grow: 1; position: relative; max-width: 800px; margin: 0 auto; width: 100%; }
input { padding: 10px 20px; font-size: 14px; border: 1px solid #dfe1e5; border-radius: 24px; width: 100%; outline: none; background: #f1f3f4; color: #202124; transition: all 0.2s; box-sizing: border-box; }
input:focus { background: #fff; box-shadow: 0 1px 6px rgba(32,33,36,.28); border-color: rgba(223,225,229,0); }
.new-tab-btn { cursor: pointer; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; color: #5f6368; margin-left: 8px; margin-bottom: 8px; transition: 0.2s; }
.new-tab-btn:hover { background: #dadce0; color: #202124; }
.iframe-container { flex-grow: 1; position: relative; background: #fff; display: none; }
iframe { border: none; width: 100%; height: 100%; position: absolute; top: 0; left: 0; display: none; background: #fff; }
iframe.active { display: block; }
.loader { position: absolute; bottom: 0; left: 0; height: 2px; background: #1a73e8; transition: width 0.3s; width: 0; z-index: 100; }
.panic-hint { position: fixed; bottom: 10px; right: 10px; font-size: 10px; color: #ccc; z-index: 1000; pointer-events: none; opacity: 0.5; }
</style>
</head>
<body>
<div id="splash" onclick="unlock()">
<h1>SCHOOLWORK</h1>
<p>ELA, SCIENCE, HISTORY, MATH, LITERATURE, SOCIAL STUDIES, AND WRITING</p>
<div class="hint">Click or press any key to hide this from your history.</div>
</div>
<div class='header' id="header">
<div class='tab-bar' id='tabBar'>
<div class='new-tab-btn' onclick='addTab()' title='New Tab'>+</div>
</div>
<div class='search-bar'>
<div class='nav-btns'>
<div class='nav-btn' id='backBtn' onclick='goBack()' title='Back'></div>
<div class='nav-btn' id='forwardBtn' onclick='goForward()' title='Forward'></div>
<div class='nav-btn' onclick='reloadTab()' title='Reload'></div>
<div class='nav-btn' onclick='goHome()' title='Home'>🏠</div>
</div>
<form onsubmit='handleSearch(event); return false;'>
<input id='searchInput' placeholder='Search Google or type a URL' autofocus autocomplete='off' />
<div id='loader' class='loader'></div>
</form>
</div>
</div>
<div class='iframe-container' id='iframeContainer'></div>
<div class="panic-hint">Press [ESC] to Panic</div>
<script>
var tabs = [];
var activeTabId = null;
const PROXY_BASE = "${proxyUrl}";
const DEFAULT_TITLE = "SCHOOLWORK: ELA, SCIENCE, HISTORY, MATH, LITERATURE, SOCIAL STUDIES, AND WRITING";
function unlock() {
document.getElementById('splash').style.opacity = '0';
setTimeout(() => {
document.getElementById('splash').style.display = 'none';
document.getElementById('header').style.display = 'flex';
document.getElementById('iframeContainer').style.display = 'block';
// History Cloaking
history.replaceState(null, '', 'https://www.google.com/search?q=educational+resources+for+students');
}, 500);
}
window.addEventListener('keydown', function(e) {
if (document.getElementById('splash').style.display !== 'none') {
unlock();
}
if (e.key === 'Escape') {
window.location.href = 'https://classroom.google.com';
}
});
function getRawUrl(proxiedUrl) {
try {
var url = new URL(proxiedUrl);
var raw = url.searchParams.get('url');
if (raw) return raw;
if (proxiedUrl.includes('/api/proxy?url=')) {
return decodeURIComponent(proxiedUrl.split('/api/proxy?url=')[1]);
}
if (proxiedUrl.includes('/api/proxy/')) {
return proxiedUrl.split('/api/proxy/')[1];
}
} catch(e) {}
return proxiedUrl;
}
function addTab(url, title) {
url = url || 'https://www.google.com/search?igu=1';
title = title || 'New Tab';
var id = 'tab-' + Math.random().toString(36).substr(2, 9);
var tabObj = {
id: id,
url: url,
title: title,
history: [url],
historyIndex: 0
};
tabs.push(tabObj);
var tabEl = document.createElement('div');
tabEl.className = 'tab';
tabEl.id = 'el-' + id;
tabEl.onclick = function() { switchTab(id); };
var titleSpan = document.createElement('span');
titleSpan.className = 'title';
titleSpan.style.flexGrow = '1';
titleSpan.style.overflow = 'hidden';
titleSpan.style.textOverflow = 'ellipsis';
titleSpan.innerText = title;
tabEl.appendChild(titleSpan);
var closeBtn = document.createElement('span');
closeBtn.className = 'close';
closeBtn.innerText = '×';
closeBtn.onclick = function(e) {
e.stopPropagation();
removeTab(id);
};
tabEl.appendChild(closeBtn);
var tabBar = document.getElementById('tabBar');
tabBar.insertBefore(tabEl, tabBar.querySelector('.new-tab-btn'));
var iframe = document.createElement('iframe');
iframe.id = 'frame-' + id;
iframe.src = PROXY_BASE + encodeURIComponent(url);
iframe.setAttribute('allow', 'autoplay; encrypted-media; camera; microphone; fullscreen; clipboard-write; geolocation');
iframe.onload = function() {
document.getElementById('loader').style.width = '0';
try {
var frameUrl = iframe.contentWindow.location.href;
var rawUrl = getRawUrl(frameUrl);
if (rawUrl && rawUrl !== 'about:blank' && !rawUrl.includes('error-overlay')) {
if (tabObj.history[tabObj.historyIndex] !== rawUrl) {
tabObj.history = tabObj.history.slice(0, tabObj.historyIndex + 1);
tabObj.history.push(rawUrl);
tabObj.historyIndex++;
}
tabObj.url = rawUrl;
if (activeTabId === id) {
document.getElementById('searchInput').value = rawUrl;
updateNavButtons();
}
if (iframe.contentDocument && iframe.contentDocument.title) {
titleSpan.innerText = iframe.contentDocument.title;
}
}
} catch(e) {}
};
document.getElementById('iframeContainer').appendChild(iframe);
switchTab(id);
return id;
}
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'zen-browser-url-change') {
var rawUrl = getRawUrl(e.data.url);
var iframe = Array.from(document.querySelectorAll('iframe')).find(f => f.contentWindow === e.source);
if (!iframe) return;
var id = iframe.id.replace('frame-', '');
var tab = tabs.find(t => t.id === id);
if (tab && rawUrl && rawUrl !== 'about:blank') {
if (tab.history[tab.historyIndex] !== rawUrl) {
tab.history = tab.history.slice(0, tab.historyIndex + 1);
tab.history.push(rawUrl);
tab.historyIndex++;
}
tab.url = rawUrl;
if (activeTabId === tab.id) {
document.getElementById('searchInput').value = rawUrl;
updateNavButtons();
}
if (e.data.title) {
var tabEl = document.getElementById('el-' + tab.id);
if (tabEl) tabEl.querySelector('.title').innerText = e.data.title;
}
}
}
});
function switchTab(id) {
activeTabId = id;
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
document.querySelectorAll('iframe').forEach(el => el.classList.remove('active'));
var el = document.getElementById('el-' + id);
var frame = document.getElementById('frame-' + id);
if (el) el.classList.add('active');
if (frame) frame.classList.add('active');
var tab = tabs.find(t => t.id === id);
if (tab) {
document.getElementById('searchInput').value = tab.url;
updateNavButtons();
}
document.title = DEFAULT_TITLE;
}
function updateNavButtons() {
var tab = tabs.find(t => t.id === activeTabId);
if (!tab) return;
document.getElementById('backBtn').classList.toggle('disabled', tab.historyIndex <= 0);
document.getElementById('forwardBtn').classList.toggle('disabled', tab.historyIndex >= tab.history.length - 1);
}
function removeTab(id) {
var index = tabs.findIndex(t => t.id === id);
if (index === -1) return;
tabs = tabs.filter(t => t.id !== id);
document.getElementById('el-' + id).remove();
document.getElementById('frame-' + id).remove();
if (activeTabId === id) {
if (tabs.length > 0) {
var nextTab = tabs[index] || tabs[index - 1];
switchTab(nextTab.id);
} else {
addTab();
}
}
}
function goBack() {
var tab = tabs.find(t => t.id === activeTabId);
if (tab && tab.historyIndex > 0) {
tab.historyIndex--;
loadUrlInTab(tab, tab.history[tab.historyIndex], true);
}
}
function goForward() {
var tab = tabs.find(t => t.id === activeTabId);
if (tab && tab.historyIndex < tab.history.length - 1) {
tab.historyIndex++;
loadUrlInTab(tab, tab.history[tab.historyIndex], true);
}
}
function loadUrlInTab(tab, url, skipHistory) {
tab.url = url;
var iframe = document.getElementById('frame-' + tab.id);
iframe.src = PROXY_BASE + encodeURIComponent(url);
if (activeTabId === tab.id) {
document.getElementById('searchInput').value = url;
}
document.getElementById('loader').style.width = '40%';
updateNavButtons();
}
function reloadTab() {
if (activeTabId) {
var frame = document.getElementById('frame-' + activeTabId);
frame.src = frame.src;
document.getElementById('loader').style.width = '50%';
}
}
function goHome() {
handleSearch({ preventDefault: () => {}, target: null }, 'https://www.google.com/search?igu=1');
}
function handleSearch(e, forcedUrl) {
if (e) e.preventDefault();
var input = document.getElementById('searchInput');
var query = forcedUrl || input.value.trim();
if (!query) return false;
var url = query;
if (!query.match(/^(https?:\\/\\/)/i)) {
if (query.includes('.') && !query.includes(' ')) {
url = 'https://' + query;
} else {
url = "https://www.google.com/search?igu=1&q=" + encodeURIComponent(query);
}
}
if (url.includes('youtube.com') && !url.includes('m.youtube.com')) {
url = url.replace('www.youtube.com', 'm.youtube.com');
}
if (!activeTabId) {
addTab(url);
} else {
var tab = tabs.find(t => t.id === activeTabId);
loadUrlInTab(tab, url);
}
input.blur();
return false;
}
addTab();
// Initial title set
document.title = DEFAULT_TITLE;
</script>
</body>
</html>
`;
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);

View File

@ -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<Tab[]>([
{ 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<Tab>) => {
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 (
<div className="flex flex-col h-screen bg-[#f8f9fa] text-slate-900 overflow-hidden font-sans">
<Head>
<title>SCHOOLWORK - Educational Resources</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</Head>
{/* Chrome-style Tab Bar */}
<div className="bg-[#dee1e6] px-2 pt-2 flex items-end gap-1 select-none overflow-x-auto no-scrollbar">
{tabs.map(tab => (
<div
key={tab.id}
onClick={() => 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'}
`}
>
<BaseIcon path={mdiShieldCheck} size={14} className={activeTabId === tab.id ? 'text-green-600' : 'text-slate-400'} />
<span className="truncate flex-grow font-medium">
{tab.title}
</span>
<button
onClick={(e) => closeTab(e, tab.id)}
className="hover:bg-gray-200 rounded-full p-0.5 opacity-0 group-hover:opacity-100"
>
<BaseIcon path={mdiClose} size={14} />
</button>
{activeTabId !== tab.id && <div className="absolute right-0 top-1/4 bottom-1/4 w-px bg-slate-400"></div>}
</div>
))}
<button
onClick={addTab}
className="mb-2 ml-1 p-1 hover:bg-[#cfd2d7] rounded-lg cursor-pointer text-slate-600"
>
<BaseIcon path={mdiPlus} size={20} />
</button>
</div>
{/* Toolbar */}
<div className="bg-white p-2 flex gap-3 items-center border-b border-[#dee1e6] shadow-sm z-20">
<div className="flex gap-1">
<button onClick={() => window.history.back()} className="p-1.5 hover:bg-gray-100 rounded-full transition-colors text-slate-600">
<BaseIcon path={mdiArrowLeft} size={20} />
</button>
<button onClick={() => window.history.forward()} className="p-1.5 hover:bg-gray-100 rounded-full transition-colors text-slate-600">
<BaseIcon path={mdiArrowRight} size={20} />
</button>
<button onClick={() => updateTab(activeTabId, { iframeUrl: activeTab.iframeUrl })} className="p-1.5 hover:bg-gray-100 rounded-full transition-colors text-slate-600">
<BaseIcon path={mdiRefresh} size={20} className={activeTab.isLoading ? 'animate-spin' : ''} />
</button>
<button onClick={goHome} className="p-1.5 hover:bg-gray-100 rounded-full transition-colors text-slate-600">
<BaseIcon path={mdiHome} size={20} />
</button>
</div>
<form
onSubmit={(e) => { 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"
>
<BaseIcon path={mdiMagnify} size={18} className="text-slate-500 mr-2" />
<input
type="text"
value={activeTab.url}
onChange={(e) => 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"
/>
</form>
<div className="flex gap-1 items-center">
<button
onClick={panic}
className="flex items-center gap-1.5 px-3 py-1.5 bg-red-50 text-red-600 hover:bg-red-100 rounded-full text-xs font-bold transition-colors border border-red-100"
>
<BaseIcon path={mdiAlertOctagon} size={16} />
PANIC (ESC)
</button>
<button className="p-2 hover:bg-gray-100 rounded-full text-slate-600">
<BaseIcon path={mdiDotsVertical} size={20} />
</button>
</div>
</div>
{/* Browser Content */}
<div className="flex-grow relative bg-white overflow-hidden">
{tabs.map(tab => (
<div key={tab.id} className={`absolute inset-0 ${tab.id === activeTabId ? 'block' : 'hidden'}`}>
{!tab.iframeUrl ? (
<div className="flex flex-col items-center justify-center h-full bg-white p-8">
<div className="max-w-2xl w-full text-center">
<h1 className="text-5xl font-bold mb-8 tracking-tight text-[#1a73e8]">
<span className="text-blue-500">S</span>
<span className="text-red-500">t</span>
<span className="text-yellow-500">u</span>
<span className="text-blue-500">d</span>
<span className="text-green-500">y</span>
<span className="text-red-500 ml-1">P</span>
<span className="text-blue-500">o</span>
<span className="text-yellow-500">r</span>
<span className="text-green-500">t</span>
<span className="text-red-500">a</span>
<span className="text-blue-500">l</span>
</h1>
<div className="relative mb-12 group">
<div className="absolute inset-y-0 left-4 flex items-center pointer-events-none">
<BaseIcon path={mdiMagnify} size={24} className="text-gray-400" />
</div>
<input
type="text"
placeholder="Search educational resources..."
className="w-full bg-white border border-gray-200 hover:shadow-md focus:shadow-md rounded-full py-4 pl-12 pr-6 outline-none transition-all text-lg"
onKeyDown={(e) => e.key === 'Enter' && navigate((e.target as HTMLInputElement).value, tab.id)}
/>
</div>
<div className="flex flex-wrap justify-center gap-6">
{QUICK_LINKS.map(link => (
<button
key={link.name}
onClick={() => navigate(link.url, tab.id)}
className="flex flex-col items-center gap-2 group"
>
<div className={`${link.color} w-14 h-14 rounded-full flex items-center justify-center shadow-sm group-hover:shadow-md transition-all text-white font-bold text-xl`}>
{link.name[0]}
</div>
<span className="text-sm text-gray-600 group-hover:underline">{link.name}</span>
</button>
))}
</div>
</div>
</div>
) : (
<>
{tab.isLoading && (
<div className="absolute inset-x-0 top-0 h-1 bg-blue-100 overflow-hidden z-50">
<div className="h-full bg-blue-500 animate-progress origin-left w-full"></div>
</div>
)}
<iframe
src={tab.iframeUrl}
className="w-full h-full border-none bg-white"
onLoad={() => {
updateTab(tab.id, { isLoading: false });
try {
const hostname = new URL(tab.displayUrl).hostname;
updateTab(tab.id, { title: hostname });
} catch(e) {}
}}
allow="autoplay; encrypted-media; fullscreen; clipboard-write; geolocation"
/>
</>
)}
</div>
))}
</div>
<style jsx global>{`
@keyframes progress {
0% { transform: scaleX(0); }
50% { transform: scaleX(0.7); }
100% { transform: scaleX(1); }
}
.animate-progress {
animation: progress 2s infinite ease-in-out;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
`}</style>
</div>
);
}
BrowserPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -1,335 +1,21 @@
import React, { useEffect, useState, useRef } from 'react';
import type { ReactElement } from 'react';
import React, { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import {
mdiPlay,
mdiPause,
mdiVolumeHigh,
mdiTuneVariant,
mdiClockOutline,
mdiCoffeeOutline,
mdiBookOpenVariant,
mdiRefresh,
mdiWeatherRainy,
mdiWaves,
mdiTree,
mdiVolumeMedium
} from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import BalatroGame from '../components/Balatro/BalatroGame';
import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
export default function WhiteNoiseHome() {
// Audio State
const [isPlaying, setIsPlaying] = useState(false);
const [volume, setVolume] = useState(0.5);
const audioRef = useRef<HTMLAudioElement | null>(null);
const sounds = [
{ id: 'white', name: 'White Noise', url: 'https://actions.google.com/sounds/v1/ambiences/white_noise.ogg', icon: mdiTuneVariant, color: 'indigo' },
{ id: 'rain', name: 'Heavy Rain', url: 'https://actions.google.com/sounds/v1/weather/rain_heavy_loud.ogg', icon: mdiWeatherRainy, color: 'blue' },
{ id: 'ocean', name: 'Ocean Waves', url: 'https://actions.google.com/sounds/v1/water/waves_crashing_on_shore.ogg', icon: mdiWaves, color: 'cyan' },
{ id: 'forest', name: 'Night Forest', url: 'https://actions.google.com/sounds/v1/ambiences/night_forest_with_insects.ogg', icon: mdiTree, color: 'green' },
];
const [currentSound, setCurrentSound] = useState(sounds[0]);
// Timer State
const [timerMinutes, setTimerMinutes] = useState(25);
const [timerSeconds, setTimerSeconds] = useState(0);
const [isTimerRunning, setIsTimerRunning] = useState(false);
const [timerMode, setTimerMode] = useState<'focus' | 'break'>('focus');
// Timer Logic
useEffect(() => {
let interval: NodeJS.Timeout;
if (isTimerRunning) {
interval = setInterval(() => {
if (timerSeconds > 0) {
setTimerSeconds(timerSeconds - 1);
} else if (timerMinutes > 0) {
setTimerMinutes(timerMinutes - 1);
setTimerSeconds(59);
} else {
// Timer finished
const nextMode = timerMode === 'focus' ? 'break' : 'focus';
setTimerMode(nextMode);
setTimerMinutes(nextMode === 'focus' ? 25 : 5);
setTimerSeconds(0);
setIsTimerRunning(false);
// Play a notification sound could be added here
}
}, 1000);
}
return () => clearInterval(interval);
}, [isTimerRunning, timerMinutes, timerSeconds, timerMode]);
const toggleTimer = () => setIsTimerRunning(!isTimerRunning);
const resetTimer = () => {
setIsTimerRunning(false);
setTimerMinutes(timerMode === 'focus' ? 25 : 5);
setTimerSeconds(0);
};
const togglePlay = () => {
if (audioRef.current) {
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play().catch(err => console.error("Audio play failed:", err));
}
setIsPlaying(!isPlaying);
}
};
const changeSound = (sound: typeof sounds[0]) => {
setCurrentSound(sound);
setIsPlaying(false);
if (audioRef.current) {
audioRef.current.load();
}
};
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseFloat(e.target.value);
setVolume(newVolume);
if (audioRef.current) {
audioRef.current.volume = newVolume;
}
};
export default function Home() {
return (
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col font-sans selection:bg-indigo-500/30">
<>
<Head>
<title>{getPageTitle('ZenNoise - Deep Focus Study Tools')}</title>
<meta name="description" content="Seamless white noise loop and study timer. Achieve deep flow state with ZenNoise." />
<title>{getPageTitle('Balatro - Poker Roguelike')}</title>
<meta name="description" content="A high-fidelity poker-inspired roguelike deck builder." />
</Head>
<audio
ref={audioRef}
src={currentSound.url}
loop
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
{/* Header */}
<nav className="flex items-center justify-between px-6 md:px-12 py-6 bg-slate-950/50 backdrop-blur-xl sticky top-0 z-50 border-b border-white/5">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg shadow-indigo-500/20 rotate-3">
<BaseIcon path={mdiTuneVariant} size={22} className="text-white" />
</div>
<span className="text-2xl font-black tracking-tighter text-white">
ZenNoise
</span>
</div>
<div className="flex items-center space-x-4 md:space-x-8">
<Link href="/login" className="hidden md:block text-sm font-bold text-slate-400 hover:text-white transition-colors uppercase tracking-widest">
Sign In
</Link>
<Link
href="/login"
className="px-6 py-2.5 bg-white hover:bg-slate-200 text-slate-950 text-sm font-bold rounded-full transition-all shadow-xl hover:scale-105 active:scale-95"
>
Dashboard
</Link>
</div>
</nav>
<main className="flex-grow container mx-auto px-4 py-12 space-y-24">
{/* Hero & Primary Player */}
<section className="flex flex-col items-center justify-center text-center space-y-12 relative py-20">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-indigo-600/10 rounded-full blur-[120px] -z-10" />
<div className="space-y-6">
<h1 className="text-5xl md:text-7xl font-black tracking-tight text-white leading-tight">
Design Your <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-purple-400">Atmosphere.</span>
</h1>
<p className="text-slate-400 text-lg md:text-xl max-w-2xl mx-auto font-medium">
Mixing high-fidelity noise loops with a precision study timer to help you reach a state of absolute focus.
</p>
</div>
<div className="flex flex-col items-center space-y-8">
<div className="relative group cursor-pointer" onClick={togglePlay}>
<div className={`absolute -inset-10 bg-indigo-500/20 rounded-full blur-3xl transition-all duration-1000 ${isPlaying ? 'animate-pulse scale-125' : 'scale-100'}`} />
<div className={`relative w-48 h-48 rounded-full flex items-center justify-center transition-all duration-700 ${
isPlaying
? 'bg-white text-slate-950 scale-105 shadow-[0_0_80px_rgba(255,255,255,0.2)]'
: 'bg-indigo-600 text-white shadow-2xl shadow-indigo-600/40'
}`}>
<BaseIcon path={isPlaying ? mdiPause : mdiPlay} size={80} />
</div>
</div>
<div className="flex items-center space-x-3 text-slate-400 font-bold uppercase tracking-widest text-xs">
<span className="w-8 h-[1px] bg-slate-800" />
<span>Now Playing: {currentSound.name}</span>
<span className="w-8 h-[1px] bg-slate-800" />
</div>
</div>
</section>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Study Timer Section */}
<section className="bg-white/5 border border-white/10 rounded-[40px] p-8 md:p-12 space-y-8 backdrop-blur-sm relative overflow-hidden group">
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:opacity-20 transition-opacity">
<BaseIcon path={mdiClockOutline} size={120} />
</div>
<div className="space-y-2">
<h2 className="text-3xl font-black text-white">Study Timer</h2>
<p className="text-slate-400 font-medium">Precision Pomodoro to keep your brain sharp.</p>
</div>
<div className="flex flex-col items-center space-y-8">
<div className="text-8xl md:text-9xl font-black tabular-nums tracking-tighter text-white">
{String(timerMinutes).padStart(2, '0')}:{String(timerSeconds).padStart(2, '0')}
</div>
<div className="flex items-center space-x-4">
<button
onClick={toggleTimer}
className={`px-10 py-4 rounded-2xl font-bold text-lg transition-all active:scale-95 ${
isTimerRunning
? 'bg-slate-800 text-white hover:bg-slate-700'
: 'bg-indigo-600 text-white hover:bg-indigo-500 shadow-xl shadow-indigo-600/20'
}`}
>
{isTimerRunning ? 'Pause' : 'Start Focus'}
</button>
<button
onClick={resetTimer}
className="p-4 bg-white/5 hover:bg-white/10 rounded-2xl text-slate-400 hover:text-white transition-all"
>
<BaseIcon path={mdiRefresh} size={28} />
</button>
</div>
<div className="flex bg-slate-900 p-1.5 rounded-2xl border border-white/5">
<button
onClick={() => { setTimerMode('focus'); setTimerMinutes(25); setTimerSeconds(0); setIsTimerRunning(false); }}
className={`px-6 py-2 rounded-xl text-sm font-bold transition-all ${timerMode === 'focus' ? 'bg-white text-slate-950 shadow-lg' : 'text-slate-500 hover:text-slate-300'}`}
>
Focus (25m)
</button>
<button
onClick={() => { setTimerMode('break'); setTimerMinutes(5); setTimerSeconds(0); setIsTimerRunning(false); }}
className={`px-6 py-2 rounded-xl text-sm font-bold transition-all ${timerMode === 'break' ? 'bg-white text-slate-950 shadow-lg' : 'text-slate-500 hover:text-slate-300'}`}
>
Break (5m)
</button>
</div>
</div>
</section>
{/* Library Section */}
<section className="bg-white/5 border border-white/10 rounded-[40px] p-8 md:p-12 space-y-8 backdrop-blur-sm relative overflow-hidden group">
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:opacity-20 transition-opacity">
<BaseIcon path={mdiBookOpenVariant} size={120} />
</div>
<div className="space-y-2">
<h2 className="text-3xl font-black text-white">Sound Library</h2>
<p className="text-slate-400 font-medium">Switch between different high-fidelity environments.</p>
</div>
<div className="grid grid-cols-2 gap-4">
{sounds.map((sound) => (
<button
key={sound.id}
onClick={() => changeSound(sound)}
className={`p-6 rounded-3xl border transition-all text-left flex flex-col space-y-4 group/card ${
currentSound.id === sound.id
? 'bg-white border-white text-slate-950'
: 'bg-white/5 border-white/10 text-white hover:bg-white/10 hover:border-white/20'
}`}
>
<div className={`w-12 h-12 rounded-2xl flex items-center justify-center transition-colors ${
currentSound.id === sound.id ? 'bg-slate-100 text-indigo-600' : 'bg-slate-900 text-slate-400 group-hover/card:text-indigo-400'
}`}>
<BaseIcon path={sound.icon} size={28} />
</div>
<div>
<div className="font-bold text-lg">{sound.name}</div>
<div className={`text-xs uppercase tracking-widest font-bold opacity-50 ${currentSound.id === sound.id ? 'text-indigo-600' : 'text-slate-400'}`}>
{sound.id === 'white' ? 'Continuous' : 'Atmospheric'}
</div>
</div>
</button>
))}
</div>
{/* Volume Control Overlay */}
<div className="pt-4">
<div className="bg-slate-900/80 backdrop-blur-xl p-6 rounded-[32px] border border-white/5 flex items-center space-x-6">
<BaseIcon path={volume > 0.5 ? mdiVolumeHigh : volume > 0 ? mdiVolumeMedium : mdiVolumeMedium} size={24} className="text-indigo-400" />
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={handleVolumeChange}
className="flex-grow h-1.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500"
/>
<span className="text-slate-400 font-mono text-xs w-8">{Math.round(volume * 100)}%</span>
</div>
</div>
</section>
</div>
{/* Benefits Section */}
<section className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center py-12">
<div className="space-y-4">
<div className="w-16 h-16 bg-indigo-500/10 rounded-2xl flex items-center justify-center mx-auto text-indigo-400">
<BaseIcon path={mdiTuneVariant} size={32} />
</div>
<h3 className="text-xl font-bold text-white">Mask Distractions</h3>
<p className="text-slate-500 leading-relaxed">Cancel out annoying background noise like construction or chatter with ease.</p>
</div>
<div className="space-y-4">
<div className="w-16 h-16 bg-purple-500/10 rounded-2xl flex items-center justify-center mx-auto text-purple-400">
<BaseIcon path={mdiClockOutline} size={32} />
</div>
<h3 className="text-xl font-bold text-white">Improve Focus</h3>
<p className="text-slate-500 leading-relaxed">Standardize your study environment to trigger &quot;flow state&quot; faster and more reliably.</p>
</div>
<div className="space-y-4">
<div className="w-16 h-16 bg-cyan-500/10 rounded-2xl flex items-center justify-center mx-auto text-cyan-400">
<BaseIcon path={mdiCoffeeOutline} size={32} />
</div>
<h3 className="text-xl font-bold text-white">Reduce Stress</h3>
<p className="text-slate-500 leading-relaxed">Gentle noise frequencies are proven to lower cortisol levels during high-stress tasks.</p>
</div>
</section>
</main>
{/* Footer */}
<footer className="py-12 px-12 border-t border-white/5 flex flex-col md:flex-row items-center justify-between text-slate-500 text-sm bg-slate-950">
<div className="flex items-center space-x-2 opacity-50 mb-6 md:mb-0">
<BaseIcon path={mdiTuneVariant} size={18} />
<span className="font-bold tracking-tighter">ZenNoise</span>
</div>
<div className="flex flex-col md:flex-row items-center md:space-x-12 space-y-4 md:space-y-0">
<p>© 2026. Designed for Focus.</p>
<div className="flex space-x-6">
<Link href="/privacy-policy" className="hover:text-indigo-400 transition-colors">Privacy</Link>
<Link href="/terms" className="hover:text-indigo-400 transition-colors">Terms</Link>
</div>
</div>
</footer>
</div>
<BalatroGame />
</>
);
}
WhiteNoiseHome.getLayout = function getLayout(page: ReactElement) {
Home.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
};