mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 19:58:38 +00:00
Changes
This commit is contained in:
parent
9eeb88e9ea
commit
81674f8094
@ -1,25 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import GlitchText from './GlitchText';
|
|
||||||
|
|
||||||
// Hacker-themed questions that any human would know
|
|
||||||
const QUESTIONS = [
|
|
||||||
{ q: "What key do you press to open a terminal? (Hint: between D and G)", answers: ["f", "f key"] },
|
|
||||||
{ q: "Complete: The Matrix has you, ___", answers: ["neo"] },
|
|
||||||
{ q: "What does 'www' stand for in a URL?", answers: ["world wide web", "worldwideweb"] },
|
|
||||||
{ q: "What color is the Matrix rain?", answers: ["green"] },
|
|
||||||
{ q: "What does 'ctrl+c' do?", answers: ["copy", "cancel", "stop", "interrupt"] },
|
|
||||||
{ q: "What number comes after 0 in binary?", answers: ["1", "one"] },
|
|
||||||
{ q: "What does 'IP' stand for in networking?", answers: ["internet protocol"] },
|
|
||||||
{ q: "What key exits most programs? (3 letters)", answers: ["esc", "escape"] },
|
|
||||||
{ q: "Red pill or blue pill - which reveals the truth?", answers: ["red", "red pill"] },
|
|
||||||
{ q: "What symbol starts most terminal commands?", answers: ["$", "/", ">", "dollar", "slash"] },
|
|
||||||
{ q: "What does 'USB' stand for?", answers: ["universal serial bus"] },
|
|
||||||
{ q: "What is localhost's IP address?", answers: ["127.0.0.1", "localhost"] },
|
|
||||||
{ q: "Complete: Hello, _____ (classic first program output)", answers: ["world"] },
|
|
||||||
{ q: "What animal is Linux's mascot?", answers: ["penguin", "tux"] },
|
|
||||||
{ q: "What does 'CPU' stand for?", answers: ["central processing unit"] },
|
|
||||||
];
|
|
||||||
|
|
||||||
const BYPASS_KEY = 'bypass-human-check';
|
const BYPASS_KEY = 'bypass-human-check';
|
||||||
const VERIFIED_KEY = 'human-verified';
|
const VERIFIED_KEY = 'human-verified';
|
||||||
@ -28,11 +8,40 @@ interface HumanVerificationProps {
|
|||||||
onVerified: () => void;
|
onVerified: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a simple math equation
|
||||||
|
const generateEquation = () => {
|
||||||
|
const operators = ['+', '-', '*'] as const;
|
||||||
|
const operator = operators[Math.floor(Math.random() * operators.length)];
|
||||||
|
|
||||||
|
let a: number, b: number, answer: number;
|
||||||
|
|
||||||
|
switch (operator) {
|
||||||
|
case '+':
|
||||||
|
a = Math.floor(Math.random() * 50) + 1;
|
||||||
|
b = Math.floor(Math.random() * 50) + 1;
|
||||||
|
answer = a + b;
|
||||||
|
break;
|
||||||
|
case '-':
|
||||||
|
a = Math.floor(Math.random() * 50) + 20;
|
||||||
|
b = Math.floor(Math.random() * (a - 1)) + 1;
|
||||||
|
answer = a - b;
|
||||||
|
break;
|
||||||
|
case '*':
|
||||||
|
a = Math.floor(Math.random() * 12) + 2;
|
||||||
|
b = Math.floor(Math.random() * 12) + 2;
|
||||||
|
answer = a * b;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { equation: `${a} ${operator} ${b}`, answer };
|
||||||
|
};
|
||||||
|
|
||||||
const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
|
const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
|
||||||
const [question] = useState(() => QUESTIONS[Math.floor(Math.random() * QUESTIONS.length)]);
|
const [{ equation, answer }, setEquation] = useState(generateEquation);
|
||||||
const [answer, setAnswer] = useState('');
|
const [userAnswer, setUserAnswer] = useState('');
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [showHint, setShowHint] = useState(false);
|
const [attempts, setAttempts] = useState(0);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
// Check for bypass in URL
|
// Check for bypass in URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -43,101 +52,208 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
|
|||||||
}
|
}
|
||||||
}, [onVerified]);
|
}, [onVerified]);
|
||||||
|
|
||||||
|
// Draw equation on canvas for added security
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// Get computed styles for theming
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
|
const primaryColor = computedStyle.getPropertyValue('--primary').trim();
|
||||||
|
const hslMatch = primaryColor.match(/[\d.]+/g);
|
||||||
|
const primaryRGB = hslMatch
|
||||||
|
? `hsl(${hslMatch[0]}, ${hslMatch[1]}%, ${hslMatch[2]}%)`
|
||||||
|
: '#00ff00';
|
||||||
|
|
||||||
|
// Clear canvas
|
||||||
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw grid lines for terminal feel
|
||||||
|
ctx.strokeStyle = 'rgba(0, 255, 0, 0.1)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let i = 0; i < canvas.width; i += 20) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(i, 0);
|
||||||
|
ctx.lineTo(i, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let i = 0; i < canvas.height; i += 20) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, i);
|
||||||
|
ctx.lineTo(canvas.width, i);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw border
|
||||||
|
ctx.strokeStyle = primaryRGB;
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeRect(2, 2, canvas.width - 4, canvas.height - 4);
|
||||||
|
|
||||||
|
// Draw equation with slight random positioning for anti-bot
|
||||||
|
const offsetX = Math.random() * 10 - 5;
|
||||||
|
const offsetY = Math.random() * 6 - 3;
|
||||||
|
|
||||||
|
ctx.font = 'bold 48px monospace';
|
||||||
|
ctx.fillStyle = primaryRGB;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
|
// Add subtle noise/distortion
|
||||||
|
const chars = equation.split('');
|
||||||
|
let xPos = canvas.width / 2 - (chars.length * 15) + offsetX;
|
||||||
|
|
||||||
|
chars.forEach((char) => {
|
||||||
|
const yOffset = Math.random() * 4 - 2;
|
||||||
|
ctx.fillText(char, xPos, canvas.height / 2 + offsetY + yOffset);
|
||||||
|
xPos += 30;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw "= ?" at the end
|
||||||
|
ctx.fillText('= ?', xPos + 20, canvas.height / 2 + offsetY);
|
||||||
|
|
||||||
|
// Add scanline effect
|
||||||
|
for (let y = 0; y < canvas.height; y += 3) {
|
||||||
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
|
||||||
|
ctx.fillRect(0, y, canvas.width, 1);
|
||||||
|
}
|
||||||
|
}, [equation]);
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const normalizedAnswer = answer.toLowerCase().trim();
|
const parsed = parseInt(userAnswer, 10);
|
||||||
|
|
||||||
if (question.answers.some(a => normalizedAnswer === a.toLowerCase())) {
|
if (!isNaN(parsed) && parsed === answer) {
|
||||||
localStorage.setItem(VERIFIED_KEY, 'true');
|
localStorage.setItem(VERIFIED_KEY, 'true');
|
||||||
onVerified();
|
onVerified();
|
||||||
} else {
|
} else {
|
||||||
setError(true);
|
setError(true);
|
||||||
setShowHint(true);
|
setAttempts(prev => prev + 1);
|
||||||
setTimeout(() => setError(false), 500);
|
setTimeout(() => {
|
||||||
|
setError(false);
|
||||||
|
// Generate new equation after 3 failed attempts
|
||||||
|
if (attempts >= 2) {
|
||||||
|
setEquation(generateEquation());
|
||||||
|
setAttempts(0);
|
||||||
|
}
|
||||||
|
}, 600);
|
||||||
|
setUserAnswer('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNewEquation = () => {
|
||||||
|
setEquation(generateEquation());
|
||||||
|
setUserAnswer('');
|
||||||
|
setError(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
className="fixed inset-0 z-[100] bg-background flex items-center justify-center p-4"
|
className="fixed inset-0 z-[100] bg-background flex items-center justify-center p-4"
|
||||||
>
|
>
|
||||||
<div className="max-w-md w-full">
|
<div className="max-w-lg w-full">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0.9, opacity: 0 }}
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
className="border-2 border-primary box-glow p-6 bg-background"
|
className="border-2 border-primary box-glow p-6 bg-background"
|
||||||
>
|
>
|
||||||
{/* ASCII Art Header */}
|
{/* Header */}
|
||||||
<pre className="font-mono text-[8px] sm:text-[10px] text-primary text-center mb-4 leading-tight">
|
|
||||||
{`╔═══════════════════════════════════════╗
|
|
||||||
║ HUMAN VERIFICATION REQUIRED ║
|
|
||||||
╚═══════════════════════════════════════╝`}
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<GlitchText
|
<pre className="font-mono text-[10px] sm:text-xs text-primary leading-tight mb-4">
|
||||||
text="ACCESS CONTROL"
|
{`┌─────────────────────────────────────┐
|
||||||
className="font-minecraft text-xl text-primary text-glow-strong"
|
│ SECURITY VERIFICATION v2.0 │
|
||||||
/>
|
│ ANTI-BOT PROTOCOL │
|
||||||
<p className="font-pixel text-xs text-foreground/60 mt-2">
|
└─────────────────────────────────────┘`}
|
||||||
Prove you are human to continue
|
</pre>
|
||||||
|
<p className="font-mono text-sm text-foreground/70">
|
||||||
|
{'>'} Solve the equation to verify humanity
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
{/* Canvas with equation */}
|
||||||
<div className="border border-primary/30 p-4 bg-background/50">
|
<div className="mb-6 flex justify-center">
|
||||||
<p className="font-pixel text-[10px] text-foreground/60 mb-2">> QUERY:</p>
|
<canvas
|
||||||
<p className="font-minecraft text-sm text-primary">{question.q}</p>
|
ref={canvasRef}
|
||||||
</div>
|
width={320}
|
||||||
|
height={100}
|
||||||
|
className="border border-primary/30 rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* Input form */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-primary font-mono">
|
||||||
|
{'>'}_
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={answer}
|
inputMode="numeric"
|
||||||
onChange={(e) => setAnswer(e.target.value)}
|
pattern="-?[0-9]*"
|
||||||
placeholder="> Enter response..."
|
value={userAnswer}
|
||||||
|
onChange={(e) => setUserAnswer(e.target.value.replace(/[^0-9-]/g, ''))}
|
||||||
|
placeholder="Enter answer"
|
||||||
autoFocus
|
autoFocus
|
||||||
className={`w-full bg-background border-2 ${
|
className={`w-full bg-background border-2 ${
|
||||||
error ? 'border-destructive animate-pulse' : 'border-primary/50'
|
error ? 'border-destructive animate-pulse' : 'border-primary/50'
|
||||||
} p-3 font-pixel text-sm text-foreground placeholder:text-foreground/30 focus:outline-none focus:border-primary transition-colors`}
|
} p-3 pl-12 font-mono text-lg text-foreground placeholder:text-foreground/30 focus:outline-none focus:border-primary transition-colors`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showHint && (
|
{error && (
|
||||||
<motion.p
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
className="font-pixel text-[10px] text-destructive"
|
className="font-mono text-sm text-destructive flex items-center gap-2"
|
||||||
>
|
>
|
||||||
ERROR: Invalid response. Retry required.
|
<span>✗</span>
|
||||||
</motion.p>
|
<span>INCORRECT - {3 - attempts} attempts remaining</span>
|
||||||
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<button
|
<div className="flex gap-3">
|
||||||
type="submit"
|
<button
|
||||||
className="w-full font-minecraft text-sm py-3 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all duration-300 box-glow"
|
type="submit"
|
||||||
>
|
disabled={!userAnswer}
|
||||||
EXECUTE
|
className="flex-1 font-minecraft text-sm py-3 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 box-glow"
|
||||||
</button>
|
>
|
||||||
|
[VERIFY]
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleNewEquation}
|
||||||
|
className="px-4 py-3 border-2 border-primary/50 text-primary/70 hover:border-primary hover:text-primary transition-all font-mono text-sm"
|
||||||
|
>
|
||||||
|
↻
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-6 pt-4 border-t border-primary/20">
|
{/* Footer */}
|
||||||
<p className="font-pixel text-[8px] text-foreground/30 text-center">
|
<div className="mt-6 pt-4 border-t border-primary/20 space-y-2">
|
||||||
// Anti-bot verification protocol v1.0
|
<div className="flex justify-between text-[10px] font-mono text-foreground/40">
|
||||||
|
<span>Protocol: MATH-VERIFY-2.0</span>
|
||||||
|
<span>Encryption: ACTIVE</span>
|
||||||
|
</div>
|
||||||
|
<p className="font-mono text-[10px] text-foreground/30 text-center">
|
||||||
|
// This verification helps protect against automated access
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Decorative terminal lines */}
|
{/* Terminal decoration */}
|
||||||
<div className="mt-4 font-mono text-[10px] text-primary/50">
|
<div className="mt-4 font-mono text-[10px] text-primary/50 space-y-1">
|
||||||
<p>> Awaiting human verification...</p>
|
<p>{'>'} Awaiting verification input...</p>
|
||||||
<p className="animate-pulse">> _</p>
|
<p className="animate-pulse">{'>'} _</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -11,12 +11,27 @@ export interface Achievement {
|
|||||||
secret?: boolean;
|
secret?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track AI message count in localStorage
|
||||||
|
const AI_MESSAGE_COUNT_KEY = 'ai-message-count';
|
||||||
|
|
||||||
|
export const incrementAiMessageCount = (): number => {
|
||||||
|
const current = parseInt(localStorage.getItem(AI_MESSAGE_COUNT_KEY) || '0', 10);
|
||||||
|
const newCount = current + 1;
|
||||||
|
localStorage.setItem(AI_MESSAGE_COUNT_KEY, newCount.toString());
|
||||||
|
return newCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAiMessageCount = (): number => {
|
||||||
|
return parseInt(localStorage.getItem(AI_MESSAGE_COUNT_KEY) || '0', 10);
|
||||||
|
};
|
||||||
|
|
||||||
interface AchievementsContextType {
|
interface AchievementsContextType {
|
||||||
achievements: Achievement[];
|
achievements: Achievement[];
|
||||||
unlockAchievement: (id: string) => void;
|
unlockAchievement: (id: string) => void;
|
||||||
getUnlockedCount: () => number;
|
getUnlockedCount: () => number;
|
||||||
getTotalCount: () => number;
|
getTotalCount: () => number;
|
||||||
timeOnSite: number;
|
timeOnSite: number;
|
||||||
|
checkAiAchievements: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultAchievements: Achievement[] = [
|
const defaultAchievements: Achievement[] = [
|
||||||
@ -214,6 +229,13 @@ export const AchievementsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
return achievements.length;
|
return achievements.length;
|
||||||
}, [achievements]);
|
}, [achievements]);
|
||||||
|
|
||||||
|
// Check AI message achievements
|
||||||
|
const checkAiAchievements = useCallback(() => {
|
||||||
|
const count = getAiMessageCount();
|
||||||
|
if (count >= 5) unlockAchievement('ai_conversation');
|
||||||
|
if (count >= 20) unlockAchievement('ai_long_chat');
|
||||||
|
}, [unlockAchievement]);
|
||||||
|
|
||||||
// Save achievements
|
// Save achievements
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('achievements', JSON.stringify(achievements));
|
localStorage.setItem('achievements', JSON.stringify(achievements));
|
||||||
@ -227,6 +249,7 @@ export const AchievementsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
getUnlockedCount,
|
getUnlockedCount,
|
||||||
getTotalCount,
|
getTotalCount,
|
||||||
timeOnSite,
|
timeOnSite,
|
||||||
|
checkAiAchievements,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ interface SettingsContextType {
|
|||||||
setCrtEnabled: (enabled: boolean) => void;
|
setCrtEnabled: (enabled: boolean) => void;
|
||||||
soundEnabled: boolean;
|
soundEnabled: boolean;
|
||||||
setSoundEnabled: (enabled: boolean) => void;
|
setSoundEnabled: (enabled: boolean) => void;
|
||||||
cryptoConsent: boolean;
|
cryptoConsent: boolean | null; // null = never asked this session
|
||||||
setCryptoConsent: (consent: boolean) => void;
|
setCryptoConsent: (consent: boolean) => void;
|
||||||
playSound: (type: SoundType) => void;
|
playSound: (type: SoundType) => void;
|
||||||
hashrate: number;
|
hashrate: number;
|
||||||
@ -16,6 +16,8 @@ interface SettingsContextType {
|
|||||||
setTotalHashes: (hashes: number) => void;
|
setTotalHashes: (hashes: number) => void;
|
||||||
acceptedHashes: number;
|
acceptedHashes: number;
|
||||||
setAcceptedHashes: (hashes: number) => void;
|
setAcceptedHashes: (hashes: number) => void;
|
||||||
|
audioBlocked: boolean;
|
||||||
|
resetAudioContext: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsContext = createContext<SettingsContextType | undefined>(undefined);
|
const SettingsContext = createContext<SettingsContextType | undefined>(undefined);
|
||||||
@ -31,11 +33,30 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
return saved !== null ? JSON.parse(saved) : true;
|
return saved !== null ? JSON.parse(saved) : true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [cryptoConsent, setCryptoConsent] = useState(() => {
|
// Crypto consent: null = needs prompt, true = accepted, false = declined
|
||||||
|
// On session start, if user previously declined, we reset to null to re-prompt
|
||||||
|
const [cryptoConsent, setCryptoConsentState] = useState<boolean | null>(() => {
|
||||||
const saved = localStorage.getItem('cryptoConsent');
|
const saved = localStorage.getItem('cryptoConsent');
|
||||||
return saved !== null ? JSON.parse(saved) : false;
|
if (saved === null) return null; // Never set
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
// If declined (false), return null to re-prompt on new session
|
||||||
|
// We use sessionStorage to track if we've already shown the prompt this session
|
||||||
|
const sessionPrompted = sessionStorage.getItem('cryptoConsentPrompted');
|
||||||
|
if (parsed === false && !sessionPrompted) {
|
||||||
|
return null; // Re-prompt
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setCryptoConsent = (consent: boolean) => {
|
||||||
|
setCryptoConsentState(consent);
|
||||||
|
localStorage.setItem('cryptoConsent', JSON.stringify(consent));
|
||||||
|
sessionStorage.setItem('cryptoConsentPrompted', 'true');
|
||||||
|
};
|
||||||
|
|
||||||
|
const [audioBlocked, setAudioBlocked] = useState(false);
|
||||||
|
const [audioFailCount, setAudioFailCount] = useState(0);
|
||||||
|
|
||||||
const [hashrate, setHashrate] = useState(0);
|
const [hashrate, setHashrate] = useState(0);
|
||||||
const [totalHashes, setTotalHashes] = useState(0);
|
const [totalHashes, setTotalHashes] = useState(0);
|
||||||
const [acceptedHashes, setAcceptedHashes] = useState(0);
|
const [acceptedHashes, setAcceptedHashes] = useState(0);
|
||||||
@ -81,9 +102,15 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
localStorage.setItem('soundEnabled', JSON.stringify(soundEnabled));
|
localStorage.setItem('soundEnabled', JSON.stringify(soundEnabled));
|
||||||
}, [soundEnabled]);
|
}, [soundEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
// Reset audio context to try again after user interaction
|
||||||
localStorage.setItem('cryptoConsent', JSON.stringify(cryptoConsent));
|
const resetAudioContext = useCallback(() => {
|
||||||
}, [cryptoConsent]);
|
if (audioContextRef.current) {
|
||||||
|
audioContextRef.current.close();
|
||||||
|
audioContextRef.current = null;
|
||||||
|
}
|
||||||
|
setAudioBlocked(false);
|
||||||
|
setAudioFailCount(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const playSound = useCallback((type: SoundType) => {
|
const playSound = useCallback((type: SoundType) => {
|
||||||
// Use ref to ensure we always have the latest value
|
// Use ref to ensure we always have the latest value
|
||||||
@ -91,6 +118,21 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const audioContext = getAudioContext();
|
const audioContext = getAudioContext();
|
||||||
|
|
||||||
|
// Check if context is blocked
|
||||||
|
if (audioContext.state === 'suspended') {
|
||||||
|
audioContext.resume().catch(() => {
|
||||||
|
setAudioFailCount(prev => {
|
||||||
|
const newCount = prev + 1;
|
||||||
|
if (newCount >= 3) {
|
||||||
|
setAudioBlocked(true);
|
||||||
|
}
|
||||||
|
return newCount;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const oscillator = audioContext.createOscillator();
|
const oscillator = audioContext.createOscillator();
|
||||||
const gainNode = audioContext.createGain();
|
const gainNode = audioContext.createGain();
|
||||||
|
|
||||||
@ -142,11 +184,23 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
oscillator.stop(now + 0.15);
|
oscillator.stop(now + 0.15);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset fail count on success
|
||||||
|
if (audioFailCount > 0) {
|
||||||
|
setAudioFailCount(0);
|
||||||
|
setAudioBlocked(false);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silently fail if audio context has issues
|
|
||||||
console.warn('Audio playback failed:', e);
|
console.warn('Audio playback failed:', e);
|
||||||
|
setAudioFailCount(prev => {
|
||||||
|
const newCount = prev + 1;
|
||||||
|
if (newCount >= 3) {
|
||||||
|
setAudioBlocked(true);
|
||||||
|
}
|
||||||
|
return newCount;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [getAudioContext]);
|
}, [getAudioContext, audioFailCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContext.Provider
|
<SettingsContext.Provider
|
||||||
@ -164,6 +218,8 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
setTotalHashes,
|
setTotalHashes,
|
||||||
acceptedHashes,
|
acceptedHashes,
|
||||||
setAcceptedHashes,
|
setAcceptedHashes,
|
||||||
|
audioBlocked,
|
||||||
|
resetAudioContext,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
|
import { useAchievements, incrementAiMessageCount } from '@/contexts/AchievementsContext';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import GlitchText from '@/components/GlitchText';
|
import GlitchText from '@/components/GlitchText';
|
||||||
import MessageContent from '@/components/MessageContent';
|
import MessageContent from '@/components/MessageContent';
|
||||||
@ -79,6 +80,7 @@ const AIChat = () => {
|
|||||||
const [tempCustomConfig, setTempCustomConfig] = useState<CustomApiConfig>({ endpoint: '', apiKey: '', model: '' });
|
const [tempCustomConfig, setTempCustomConfig] = useState<CustomApiConfig>({ endpoint: '', apiKey: '', model: '' });
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const { playSound } = useSettings();
|
const { playSound } = useSettings();
|
||||||
|
const { checkAiAchievements } = useAchievements();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Persist messages to localStorage
|
// Persist messages to localStorage
|
||||||
@ -135,6 +137,10 @@ const AIChat = () => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
playSound('click');
|
playSound('click');
|
||||||
|
|
||||||
|
// Track AI message count for achievements
|
||||||
|
incrementAiMessageCount();
|
||||||
|
checkAiAchievements();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build chat history, filtering out empty messages, system notices, and ensuring proper alternation
|
// Build chat history, filtering out empty messages, system notices, and ensuring proper alternation
|
||||||
const validMessages = messages.filter(msg => msg.content.trim() !== '' && msg.role !== 'system');
|
const validMessages = messages.filter(msg => msg.content.trim() !== '' && msg.role !== 'system');
|
||||||
|
|||||||
@ -33,7 +33,8 @@ const Index = () => {
|
|||||||
});
|
});
|
||||||
const [showConsentModal, setShowConsentModal] = useState(false);
|
const [showConsentModal, setShowConsentModal] = useState(false);
|
||||||
const [konamiActive, setKonamiActive] = useState(false);
|
const [konamiActive, setKonamiActive] = useState(false);
|
||||||
const { crtEnabled, playSound } = useSettings();
|
const [audioPromptDismissed, setAudioPromptDismissed] = useState(false);
|
||||||
|
const { crtEnabled, playSound, cryptoConsent, audioBlocked, resetAudioContext, setSoundEnabled } = useSettings();
|
||||||
const { unlockAchievement } = useAchievements();
|
const { unlockAchievement } = useAchievements();
|
||||||
const { activated: konamiActivated, reset: resetKonami } = useKonamiCode();
|
const { activated: konamiActivated, reset: resetKonami } = useKonamiCode();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -77,15 +78,47 @@ const Index = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
// Show consent modal after loading if user hasn't made a choice yet
|
// Show consent modal after loading if user hasn't accepted yet (null = needs prompt)
|
||||||
const hasSeenConsent = localStorage.getItem('cryptoConsentSeen');
|
if (cryptoConsent === null) {
|
||||||
if (!hasSeenConsent) {
|
|
||||||
setShowConsentModal(true);
|
setShowConsentModal(true);
|
||||||
}
|
}
|
||||||
}, 3000); // Extended to 3s for boot sequence
|
}, 3000); // Extended to 3s for boot sequence
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, [cryptoConsent]);
|
||||||
|
|
||||||
|
// Show audio blocked prompt
|
||||||
|
useEffect(() => {
|
||||||
|
if (audioBlocked && !audioPromptDismissed) {
|
||||||
|
toast({
|
||||||
|
title: "Sound Effects Blocked",
|
||||||
|
description: "Click anywhere or disable sounds in settings",
|
||||||
|
action: (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
resetAudioContext();
|
||||||
|
setAudioPromptDismissed(true);
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 text-xs border border-primary rounded hover:bg-primary/20"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSoundEnabled(false);
|
||||||
|
setAudioPromptDismissed(true);
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 text-xs border border-primary rounded hover:bg-primary/20"
|
||||||
|
>
|
||||||
|
Disable
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
duration: 10000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [audioBlocked, audioPromptDismissed, resetAudioContext, setSoundEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isRedTheme) {
|
if (isRedTheme) {
|
||||||
@ -104,17 +137,21 @@ const Index = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConsentClose = () => {
|
const handleConsentClose = () => {
|
||||||
localStorage.setItem('cryptoConsentSeen', 'true');
|
|
||||||
setShowConsentModal(false);
|
setShowConsentModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVerified = () => {
|
||||||
|
setIsVerified(true);
|
||||||
|
unlockAchievement('verified_human');
|
||||||
|
};
|
||||||
|
|
||||||
// Show verification gate if not verified
|
// Show verification gate if not verified
|
||||||
if (!isVerified) {
|
if (!isVerified) {
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-screen overflow-x-hidden ${crtEnabled ? 'crt' : ''}`}>
|
<div className={`min-h-screen overflow-x-hidden ${crtEnabled ? 'crt' : ''}`}>
|
||||||
<MatrixRain color={isRedTheme ? '#FF0000' : '#00FF00'} />
|
<MatrixRain color={isRedTheme ? '#FF0000' : '#00FF00'} />
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<HumanVerification onVerified={() => setIsVerified(true)} />
|
<HumanVerification onVerified={handleVerified} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user