Files
s21_game2_front_dan/app/(home)/main/page.tsx
2025-12-23 13:02:38 +03:00

985 lines
46 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect, useRef } from "react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Lock,
CheckCircle2,
Circle,
Zap,
Target,
Shield,
Code,
Database,
Globe,
X,
Search,
Filter,
Trophy,
Clock,
Star,
Sparkles,
Award,
TrendingUp
} from "lucide-react"
import Link from "next/link"
interface Particle {
x: number
y: number
vx: number
vy: number
size: number
opacity: number
}
export default function MainPage() {
const [selectedQuest, setSelectedQuest] = useState<number | null>(null)
const [hoveredQuest, setHoveredQuest] = useState<number | null>(null)
const [mousePos, setMousePos] = useState({ x: 0, y: 0 })
const [particles, setParticles] = useState<Particle[]>([])
const [searchQuery, setSearchQuery] = useState("")
const [filterDifficulty, setFilterDifficulty] = useState<string>("all")
const canvasRef = useRef<HTMLCanvasElement>(null)
const [stats] = useState({
xpToday: 450,
timeSpent: "2h 15m",
streak: 7,
levelProgress: 68
})
const quests = [
{
id: 1,
name: "Registration",
status: "completed",
icon: Circle,
color: "cyan",
description: "Создайте свой аккаунт и начните путешествие в мир киберпространства",
reward: 100,
difficulty: "Easy",
estimatedTime: "5 min",
prerequisites: []
},
{
id: 2,
name: "Sanctum",
status: "completed",
icon: Shield,
color: "cyan",
description: "Пройдите базовую защиту системы и изучите основы безопасности",
reward: 150,
difficulty: "Easy",
estimatedTime: "15 min",
prerequisites: [1]
},
{
id: 3,
name: "Memories",
status: "unlocked",
icon: Database,
color: "yellow",
description: "Восстановите утерянные данные из архива и раскройте секреты прошлого",
reward: 200,
difficulty: "Medium",
estimatedTime: "30 min",
prerequisites: [1, 2]
},
{
id: 4,
name: "Cyber toy",
status: "locked",
icon: Code,
color: "red",
description: "Взломайте защищенную систему используя продвинутые техники",
reward: 250,
difficulty: "Medium",
estimatedTime: "45 min",
prerequisites: [3]
},
{
id: 5,
name: "Flood",
status: "locked",
icon: Zap,
color: "red",
description: "Преодолейте мощные потоки данных и найдите путь через хаос",
reward: 300,
difficulty: "Hard",
estimatedTime: "1h",
prerequisites: [3]
},
{
id: 6,
name: "Core",
status: "locked",
icon: Target,
color: "red",
description: "Проникните в самое ядро системы и получите контроль",
reward: 350,
difficulty: "Hard",
estimatedTime: "1h 30m",
prerequisites: [4, 5]
},
{
id: 7,
name: "Access point",
status: "locked",
icon: Globe,
color: "red",
description: "Получите доступ к финальной точке и завершите миссию",
reward: 500,
difficulty: "Expert",
estimatedTime: "2h",
prerequisites: [6]
},
{
id: 8,
name: "???",
status: "locked",
icon: Lock,
color: "gray",
description: "Секретное задание. Требования неизвестны.",
reward: 999,
difficulty: "???",
estimatedTime: "???",
prerequisites: [7]
}
]
const questPositions = [
{ x: 150, y: 250 },
{ x: 300, y: 250 },
{ x: 450, y: 250 },
{ x: 600, y: 250 },
{ x: 750, y: 150 },
{ x: 750, y: 350 },
{ x: 900, y: 250 },
{ x: 1050, y: 250 }
]
// Initialize particles
useEffect(() => {
const initParticles: Particle[] = []
for (let i = 0; i < 80; i++) {
initParticles.push({
x: Math.random() * (typeof window !== 'undefined' ? window.innerWidth : 1920),
y: Math.random() * (typeof window !== 'undefined' ? window.innerHeight : 1080),
vx: (Math.random() - 0.5) * 0.8,
vy: (Math.random() - 0.5) * 0.8,
size: Math.random() * 3 + 1,
opacity: Math.random() * 0.6 + 0.2
})
}
setParticles(initParticles)
}, [])
// Animate particles
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const updateCanvasSize = () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
updateCanvasSize()
let animationFrameId: number
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
setParticles(prevParticles => {
return prevParticles.map(particle => {
let newX = particle.x + particle.vx
let newY = particle.y + particle.vy
if (newX < 0 || newX > canvas.width) particle.vx *= -1
if (newY < 0 || newY > canvas.height) particle.vy *= -1
newX = Math.max(0, Math.min(canvas.width, newX))
newY = Math.max(0, Math.min(canvas.height, newY))
// Draw particle with glow
ctx.shadowBlur = 10
ctx.shadowColor = 'rgba(6, 182, 212, 0.8)'
ctx.fillStyle = `rgba(6, 182, 212, ${particle.opacity})`
ctx.beginPath()
ctx.arc(newX, newY, particle.size, 0, Math.PI * 2)
ctx.fill()
return { ...particle, x: newX, y: newY }
})
})
animationFrameId = requestAnimationFrame(animate)
}
animate()
window.addEventListener('resize', updateCanvasSize)
return () => {
cancelAnimationFrame(animationFrameId)
window.removeEventListener('resize', updateCanvasSize)
}
}, [])
// Mouse parallax
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMousePos({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
const getStatusColor = (status: string) => {
switch (status) {
case "completed": return "from-green-500 to-emerald-600"
case "unlocked": return "from-yellow-500 to-amber-600"
case "locked": return "from-red-500 to-rose-600"
default: return "from-gray-500 to-gray-600"
}
}
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case "Easy": return "text-green-400"
case "Medium": return "text-yellow-400"
case "Hard": return "text-orange-400"
case "Expert": return "text-red-400"
default: return "text-gray-400"
}
}
const filteredQuests = quests.filter(quest => {
const matchesSearch = quest.name.toLowerCase().includes(searchQuery.toLowerCase())
const matchesDifficulty = filterDifficulty === "all" || quest.difficulty === filterDifficulty
return matchesSearch && matchesDifficulty
})
return (
<div className="min-h-screen bg-[#0a0e1a] relative overflow-hidden">
{/* Animated particles canvas */}
<canvas
ref={canvasRef}
className="fixed inset-0 pointer-events-none z-0"
style={{ opacity: 0.5 }}
/>
{/* Animated background effects */}
<div
className="fixed w-[500px] h-[500px] bg-cyan-500/10 rounded-full blur-[150px] animate-pulse transition-transform duration-1000"
style={{
top: '10%',
left: '15%',
transform: `translate(${(mousePos.x - (typeof window !== 'undefined' ? window.innerWidth : 1920) / 2) * 0.03}px, ${(mousePos.y - (typeof window !== 'undefined' ? window.innerHeight : 1080) / 2) * 0.03}px)`
}}
/>
<div
className="fixed w-[500px] h-[500px] bg-purple-500/10 rounded-full blur-[150px] animate-pulse transition-transform duration-1000"
style={{
bottom: '10%',
right: '15%',
animationDelay: '1s',
transform: `translate(${-(mousePos.x - (typeof window !== 'undefined' ? window.innerWidth : 1920) / 2) * 0.03}px, ${-(mousePos.y - (typeof window !== 'undefined' ? window.innerHeight : 1080) / 2) * 0.03}px)`
}}
/>
<div
className="fixed w-[400px] h-[400px] bg-blue-500/8 rounded-full blur-[120px] animate-pulse transition-transform duration-1000"
style={{
top: '50%',
left: '50%',
animationDelay: '0.5s',
transform: `translate(-50%, -50%) translate(${(mousePos.x - (typeof window !== 'undefined' ? window.innerWidth : 1920) / 2) * 0.02}px, ${(mousePos.y - (typeof window !== 'undefined' ? window.innerHeight : 1080) / 2) * 0.02}px)`
}}
/>
{/* Header */}
<header className="bg-[#0d1117]/80 backdrop-blur-xl border-b border-cyan-500/30 sticky top-0 z-50 shadow-lg shadow-cyan-500/5">
<nav className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-8">
<div className="flex items-center gap-2">
<Sparkles className="w-5 h-5 text-cyan-400 animate-pulse" />
<span className="text-cyan-400 font-mono font-bold">CYBER ACADEMY</span>
</div>
</div>
<div className="flex items-center gap-12">
<button className="text-sm font-bold tracking-[0.2em] transition-all duration-200 font-mono uppercase text-cyan-400 scale-110 drop-shadow-[0_0_8px_rgba(6,182,212,0.5)] cursor-pointer">
Main
</button>
<Link href="/main/progress">
<button className="text-sm font-bold tracking-[0.2em] transition-all duration-200 font-mono uppercase text-cyan-500/50 hover:text-cyan-400/80 hover:scale-105 cursor-pointer">
Progress
</button>
</Link>
<Link href="/main/profile">
<button className="text-sm font-bold tracking-[0.2em] transition-all duration-200 font-mono uppercase text-cyan-500/50 hover:text-cyan-400/80 hover:scale-105 cursor-pointer">
Profile
</button>
</Link>
<Link href="/main/rules">
<button className="text-sm font-bold tracking-[0.2em] transition-all duration-200 font-mono uppercase text-cyan-500/50 hover:text-cyan-400/80 hover:scale-105 cursor-pointer">
Rules
</button>
</Link>
<button className="text-sm font-bold tracking-[0.2em] transition-all duration-200 font-mono uppercase text-cyan-500/50 hover:text-cyan-400/80 hover:scale-105 cursor-pointer">
Logoff
</button>
</div>
</div>
</nav>
</header>
{/* Stats Bar */}
<div className="container mx-auto px-6 py-4 relative z-10">
<div className="grid grid-cols-4 gap-4">
<Card className="bg-[#0d1117]/50 backdrop-blur-xl border border-cyan-500/30 p-4 shadow-[0_0_20px_rgba(6,182,212,0.1)] hover:shadow-[0_0_30px_rgba(6,182,212,0.2)] transition-all cursor-pointer hover:scale-[1.02]">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-cyan-500/20 rounded-lg flex items-center justify-center">
<Trophy className="w-5 h-5 text-cyan-400" />
</div>
<div>
<div className="text-xs text-cyan-400/60 font-mono">XP Today</div>
<div className="text-lg font-bold text-cyan-400 font-mono">+{stats.xpToday}</div>
</div>
</div>
</Card>
<Card className="bg-[#0d1117]/50 backdrop-blur-xl border border-cyan-500/30 p-4 shadow-[0_0_20px_rgba(6,182,212,0.1)] hover:shadow-[0_0_30px_rgba(6,182,212,0.2)] transition-all cursor-pointer hover:scale-[1.02]">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-cyan-500/20 rounded-lg flex items-center justify-center">
<Clock className="w-5 h-5 text-cyan-400" />
</div>
<div>
<div className="text-xs text-cyan-400/60 font-mono">Time Spent</div>
<div className="text-lg font-bold text-cyan-400 font-mono">{stats.timeSpent}</div>
</div>
</div>
</Card>
<Card className="bg-[#0d1117]/50 backdrop-blur-xl border border-cyan-500/30 p-4 shadow-[0_0_20px_rgba(6,182,212,0.1)] hover:shadow-[0_0_30px_rgba(6,182,212,0.2)] transition-all cursor-pointer hover:scale-[1.02]">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-cyan-500/20 rounded-lg flex items-center justify-center">
<Zap className="w-5 h-5 text-yellow-400 animate-pulse" />
</div>
<div>
<div className="text-xs text-cyan-400/60 font-mono">Streak</div>
<div className="text-lg font-bold text-yellow-400 font-mono">{stats.streak} days</div>
</div>
</div>
</Card>
<Card className="bg-[#0d1117]/50 backdrop-blur-xl border border-cyan-500/30 p-4 shadow-[0_0_20px_rgba(6,182,212,0.1)] hover:shadow-[0_0_30px_rgba(6,182,212,0.2)] transition-all cursor-pointer hover:scale-[1.02]">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-cyan-500/20 rounded-lg flex items-center justify-center">
<Star className="w-5 h-5 text-cyan-400" />
</div>
<div className="flex-1">
<div className="text-xs text-cyan-400/60 font-mono mb-1">Level Progress</div>
<div className="h-2 bg-[#0a0e1a] rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-cyan-500 to-blue-500 rounded-full transition-all duration-500 relative"
style={{ width: `${stats.levelProgress}%` }}
>
<div className="absolute inset-0 bg-white/20 animate-pulse" />
</div>
</div>
</div>
</div>
</Card>
</div>
</div>
{/* Main Content */}
<main className="container mx-auto px-6 py-4 relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Left Sidebar */}
<Card className="lg:col-span-1 bg-[#0d1117]/50 backdrop-blur-xl border-2 border-cyan-500/30 p-6 shadow-[0_0_30px_rgba(6,182,212,0.1)] max-h-[calc(100vh-350px)] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold text-cyan-400 font-mono flex items-center gap-2">
<Target className="w-5 h-5 animate-pulse" />
Missions
</h3>
<div className="flex gap-2">
<button className="text-cyan-400 hover:text-cyan-300 transition-colors cursor-pointer hover:scale-110">
<Search className="w-4 h-4" />
</button>
<button className="text-cyan-400 hover:text-cyan-300 transition-colors cursor-pointer hover:scale-110">
<Filter className="w-4 h-4" />
</button>
</div>
</div>
{/* Search */}
<div className="mb-4">
<Input
type="text"
placeholder="Search missions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-[#0a0e1a]/80 border-cyan-500/40 text-cyan-400 placeholder:text-cyan-400/30 font-mono text-sm cursor-text focus:border-cyan-400 transition-all"
/>
</div>
{/* Filter */}
<div className="mb-4">
<select
value={filterDifficulty}
onChange={(e) => setFilterDifficulty(e.target.value)}
className="w-full bg-[#0a0e1a]/80 border border-cyan-500/40 text-cyan-400 font-mono text-sm rounded-lg p-2 focus:border-cyan-400 focus:outline-none cursor-pointer hover:border-cyan-400/60 transition-all"
>
<option value="all">All Difficulties</option>
<option value="Easy">Easy</option>
<option value="Medium">Medium</option>
<option value="Hard">Hard</option>
<option value="Expert">Expert</option>
</select>
</div>
<div className="space-y-2">
{filteredQuests.map((quest) => {
const Icon = quest.icon
return (
<button
key={quest.id}
onClick={() => setSelectedQuest(quest.id)}
className={`w-full text-left p-3 rounded-lg border transition-all duration-300 group cursor-pointer ${
selectedQuest === quest.id
? 'bg-cyan-500/20 border-cyan-500/60 shadow-[0_0_20px_rgba(6,182,212,0.2)] scale-[1.02]'
: quest.status === 'completed'
? 'bg-green-500/5 border-green-500/30 hover:border-green-500/50 hover:scale-[1.01]'
: quest.status === 'unlocked'
? 'bg-yellow-500/5 border-yellow-500/30 hover:border-yellow-500/50 hover:scale-[1.01]'
: 'bg-[#0a0e1a]/50 border-cyan-500/20 hover:border-cyan-500/30 hover:scale-[1.01]'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-lg bg-gradient-to-br ${getStatusColor(quest.status)}/20 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform relative overflow-hidden`}>
<div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<Icon className={`w-4 h-4 relative z-10 ${
quest.status === 'completed' ? 'text-green-400' :
quest.status === 'unlocked' ? 'text-yellow-400' :
'text-red-400/50'
}`} />
</div>
<div className="flex-1 min-w-0">
<div className="font-mono text-sm text-cyan-400 truncate group-hover:text-cyan-300 transition-colors">
{quest.name}
</div>
<div className="text-xs text-cyan-400/50 font-mono">
{quest.difficulty} {quest.reward} pts
</div>
</div>
{quest.status === 'completed' && <CheckCircle2 className="w-4 h-4 text-green-400 group-hover:scale-110 transition-transform" />}
{quest.status === 'unlocked' && <Zap className="w-4 h-4 text-yellow-400 animate-pulse" />}
{quest.status === 'locked' && <Lock className="w-4 h-4 text-red-400/50" />}
</div>
</button>
)
})}
</div>
</Card>
{/* Center - Interactive Quest Map */}
<Card className="lg:col-span-3 bg-[#0d1117]/50 backdrop-blur-xl border-2 border-cyan-500/30 p-8 shadow-[0_0_30px_rgba(6,182,212,0.1)] relative overflow-hidden group min-h-[600px]">
{/* Animated background pattern */}
<div className="absolute inset-0 opacity-10">
<div className="absolute inset-0" style={{
backgroundImage: `radial-gradient(circle at 2px 2px, rgba(6, 182, 212, 0.3) 1px, transparent 0)`,
backgroundSize: '40px 40px',
animation: 'moveBackground 20s linear infinite'
}} />
</div>
<div className="relative z-10">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<h3 className="text-2xl font-bold text-cyan-400 font-mono flex items-center gap-3">
<Globe className="w-7 h-7 animate-pulse" />
Quest Network
</h3>
<div className="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 rounded-lg">
<div className="text-sm text-cyan-400/60 font-mono flex items-center gap-2">
<Award className="w-4 h-4" />
{quests.filter(q => q.status === 'completed').length}/{quests.length} Completed
</div>
</div>
</div>
<div className="flex items-center gap-3 px-4 py-2 bg-green-500/10 border border-green-500/30 rounded-lg">
<TrendingUp className="w-5 h-5 text-green-400" />
<div>
<div className="text-xs text-green-400/60 font-mono">Total XP</div>
<div className="text-sm font-bold text-green-400 font-mono">
{quests.filter(q => q.status === 'completed').reduce((acc, q) => acc + q.reward, 0)} / {quests.reduce((acc, q) => acc + q.reward, 0)}
</div>
</div>
</div>
</div>
{/* SVG Quest Map */}
<div className="relative min-h-[500px] flex items-center justify-center">
<svg
viewBox="0 0 1200 500"
className="w-full h-full"
style={{ filter: 'drop-shadow(0 0 15px rgba(6, 182, 212, 0.4))' }}
>
<defs>
{/* Enhanced gradients */}
<linearGradient id="lineGradient1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#10b981" stopOpacity="0.9" />
<stop offset="50%" stopColor="#34d399" stopOpacity="0.7" />
<stop offset="100%" stopColor="#10b981" stopOpacity="0.4" />
</linearGradient>
<linearGradient id="lineGradient2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#eab308" stopOpacity="0.7" />
<stop offset="50%" stopColor="#f59e0b" stopOpacity="0.5" />
<stop offset="100%" stopColor="#ef4444" stopOpacity="0.3" />
</linearGradient>
<linearGradient id="lineGradient3" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#ef4444" stopOpacity="0.5" />
<stop offset="100%" stopColor="#7f1d1d" stopOpacity="0.2" />
</linearGradient>
{/* Enhanced glow filter */}
<filter id="glow">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
{/* Pulsing filter */}
<filter id="pulse">
<feGaussianBlur stdDeviation="2" result="blur"/>
<feFlood floodColor="#06b6d4" floodOpacity="0.5" result="color"/>
<feComposite in="color" in2="blur" operator="in" result="glow"/>
<feMerge>
<feMergeNode in="glow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
{/* Animated energy flow particles */}
{quests.slice(0, -1).map((quest, index) => {
if (quest.status === 'completed' && quests[index + 1]) {
const start = questPositions[index]
const end = questPositions[index + 1]
return (
<g key={`flow-${index}`}>
<circle r="4" fill="#10b981" opacity="0.9" filter="url(#glow)">
<animateMotion
dur="1.5s"
repeatCount="indefinite"
path={`M ${start.x} ${start.y} L ${end.x} ${end.y}`}
/>
</circle>
<circle r="3" fill="#34d399" opacity="0.7">
<animateMotion
dur="1.5s"
repeatCount="indefinite"
begin="0.3s"
path={`M ${start.x} ${start.y} L ${end.x} ${end.y}`}
/>
</circle>
<circle r="2" fill="#6ee7b7" opacity="0.5">
<animateMotion
dur="1.5s"
repeatCount="indefinite"
begin="0.6s"
path={`M ${start.x} ${start.y} L ${end.x} ${end.y}`}
/>
</circle>
</g>
)
}
return null
})}
{/* Enhanced Connection Lines */}
<g strokeLinecap="round">
<path d="M 150 250 L 300 250" stroke="url(#lineGradient1)" strokeWidth="4" opacity="0.8" />
<path d="M 300 250 L 450 250" stroke="url(#lineGradient1)" strokeWidth="4" opacity="0.8" />
<path d="M 450 250 L 600 250" stroke="url(#lineGradient2)" strokeWidth="3" strokeDasharray="8,4">
<animate attributeName="stroke-dashoffset" from="0" to="12" dur="1s" repeatCount="indefinite" />
</path>
<path d="M 600 250 L 750 150" stroke="url(#lineGradient3)" strokeWidth="3" strokeDasharray="8,4" opacity="0.6">
<animate attributeName="stroke-dashoffset" from="0" to="12" dur="1s" repeatCount="indefinite" />
</path>
<path d="M 600 250 L 750 350" stroke="url(#lineGradient3)" strokeWidth="3" strokeDasharray="8,4" opacity="0.6">
<animate attributeName="stroke-dashoffset" from="0" to="12" dur="1s" repeatCount="indefinite" />
</path>
<path d="M 750 150 L 900 250" stroke="url(#lineGradient3)" strokeWidth="3" strokeDasharray="8,4" opacity="0.6">
<animate attributeName="stroke-dashoffset" from="0" to="12" dur="1s" repeatCount="indefinite" />
</path>
<path d="M 750 350 L 900 250" stroke="url(#lineGradient3)" strokeWidth="3" strokeDasharray="8,4" opacity="0.6">
<animate attributeName="stroke-dashoffset" from="0" to="12" dur="1s" repeatCount="indefinite" />
</path>
<path d="M 900 250 L 1050 250" stroke="url(#lineGradient3)" strokeWidth="3" strokeDasharray="8,4" opacity="0.5">
<animate attributeName="stroke-dashoffset" from="0" to="12" dur="1s" repeatCount="indefinite" />
</path>
</g>
{/* Quest Nodes - Enhanced Hexagons */}
{questPositions.map((pos, index) => {
const quest = quests[index]
const isSelected = selectedQuest === quest.id
const isHovered = hoveredQuest === quest.id
const statusColor =
quest.status === 'completed' ? '#10b981' :
quest.status === 'unlocked' ? '#eab308' :
'#ef4444'
const hexSize = isHovered ? 35 : 30
const hexPoints = Array.from({ length: 6 }, (_, i) => {
const angle = (Math.PI / 3) * i
return `${pos.x + hexSize * Math.cos(angle)},${pos.y + hexSize * Math.sin(angle)}`
}).join(' ')
return (
<g
key={index}
onMouseEnter={() => setHoveredQuest(quest.id)}
onMouseLeave={() => setHoveredQuest(null)}
onClick={() => setSelectedQuest(quest.id)}
className="cursor-pointer transition-all duration-300"
style={{
transform: isHovered ? 'scale(1.1)' : 'scale(1)',
transformOrigin: `${pos.x}px ${pos.y}px`,
transition: 'transform 0.3s ease'
}}
>
{/* Multiple glow rings for selected */}
{isSelected && (
<>
<polygon
points={Array.from({ length: 6 }, (_, i) => {
const angle = (Math.PI / 3) * i
return `${pos.x + 55 * Math.cos(angle)},${pos.y + 55 * Math.sin(angle)}`
}).join(' ')}
fill="none"
stroke={statusColor}
strokeWidth="2"
opacity="0.2"
>
<animate attributeName="opacity" values="0.2;0.5;0.2" dur="2s" repeatCount="indefinite" />
</polygon>
<polygon
points={Array.from({ length: 6 }, (_, i) => {
const angle = (Math.PI / 3) * i
return `${pos.x + 50 * Math.cos(angle)},${pos.y + 50 * Math.sin(angle)}`
}).join(' ')}
fill="none"
stroke={statusColor}
strokeWidth="2"
opacity="0.4"
>
<animate attributeName="opacity" values="0.4;0.7;0.4" dur="1.5s" repeatCount="indefinite" />
</polygon>
<polygon
points={Array.from({ length: 6 }, (_, i) => {
const angle = (Math.PI / 3) * i
return `${pos.x + 43 * Math.cos(angle)},${pos.y + 43 * Math.sin(angle)}`
}).join(' ')}
fill="none"
stroke={statusColor}
strokeWidth="3"
opacity="0.7"
/>
</>
)}
{/* Outer hexagon with gradient */}
<polygon
points={hexPoints}
fill={`url(#${quest.status === 'completed' ? 'lineGradient1' : quest.status === 'unlocked' ? 'lineGradient2' : 'lineGradient3'})`}
fillOpacity="0.15"
stroke={statusColor}
strokeWidth={isSelected ? "4" : isHovered ? "3" : "2"}
filter="url(#glow)"
className="transition-all duration-300"
/>
{/* Inner hexagon */}
<polygon
points={Array.from({ length: 6 }, (_, i) => {
const angle = (Math.PI / 3) * i
const innerSize = isHovered ? 23 : 20
return `${pos.x + innerSize * Math.cos(angle)},${pos.y + innerSize * Math.sin(angle)}`
}).join(' ')}
fill={statusColor}
opacity={quest.status === 'locked' ? '0.3' : '0.7'}
filter="url(#pulse)"
>
{quest.status === 'unlocked' && (
<animate attributeName="opacity" values="0.5;0.9;0.5" dur="2s" repeatCount="indefinite" />
)}
</polygon>
{/* Enhanced hover tooltip */}
{isHovered && (
<g>
<rect
x={pos.x - 120}
y={pos.y - 100}
width="240"
height="80"
rx="12"
fill="#0d1117"
fillOpacity="0.98"
stroke={statusColor}
strokeWidth="2"
filter="url(#glow)"
/>
<rect
x={pos.x - 115}
y={pos.y - 95}
width="230"
height="70"
rx="10"
fill={statusColor}
fillOpacity="0.05"
/>
<text
x={pos.x}
y={pos.y - 68}
textAnchor="middle"
fill="#06b6d4"
fontSize="16"
fontWeight="bold"
fontFamily="monospace"
>
{quest.name}
</text>
<text
x={pos.x}
y={pos.y - 48}
textAnchor="middle"
fill={statusColor}
fontSize="11"
fontFamily="monospace"
fontWeight="600"
>
{quest.difficulty} {quest.estimatedTime}
</text>
<text
x={pos.x}
y={pos.y - 30}
textAnchor="middle"
fill="#10b981"
fontSize="12"
fontWeight="bold"
fontFamily="monospace"
>
+{quest.reward} XP
</text>
</g>
)}
</g>
)
})}
</svg>
{/* Enhanced Quest Details Panel */}
{selectedQuest && (
<Card className="absolute bottom-4 left-4 right-4 bg-[#0a0e1a]/98 backdrop-blur-xl border-2 border-cyan-500/50 p-6 shadow-[0_0_60px_rgba(6,182,212,0.3)] animate-in fade-in slide-in-from-bottom-4 duration-300">
{(() => {
const quest = quests.find(q => q.id === selectedQuest)
if (!quest) return null
const Icon = quest.icon
return (
<>
{/* Close Button */}
<button
onClick={(e) => {
e.stopPropagation()
setSelectedQuest(null)
}}
className="absolute top-4 right-4 w-9 h-9 bg-red-500/20 hover:bg-red-500/30 rounded-lg flex items-center justify-center transition-all group border border-red-500/40 hover:border-red-500/60 cursor-pointer hover:scale-110"
>
<X className="w-5 h-5 text-red-400 group-hover:text-red-300" />
</button>
<div className="flex items-start gap-6">
<div className={`w-24 h-24 rounded-2xl bg-gradient-to-br ${getStatusColor(quest.status)}/20 border-2 border-cyan-500/40 flex items-center justify-center flex-shrink-0 shadow-lg relative overflow-hidden group cursor-pointer hover:scale-105 transition-transform`}>
<div className="absolute inset-0 bg-gradient-to-br from-cyan-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="absolute inset-0 bg-gradient-to-br from-white/5 via-transparent to-transparent" />
<Icon className={`w-12 h-12 relative z-10 ${
quest.status === 'completed' ? 'text-green-400' :
quest.status === 'unlocked' ? 'text-yellow-400 animate-pulse' :
'text-red-400/50'
}`} />
</div>
<div className="flex-1">
<div className="flex items-start justify-between mb-3">
<div>
<h4 className="text-2xl font-bold text-cyan-400 font-mono mb-2 flex items-center gap-3">
{quest.name}
{quest.status === 'completed' && (
<span className="text-xs bg-green-500/20 text-green-400 px-2 py-1 rounded-full border border-green-500/40">
CLEARED
</span>
)}
</h4>
<div className="flex items-center gap-4 mb-3">
<span className={`text-xs font-mono ${getDifficultyColor(quest.difficulty)} font-bold px-3 py-1.5 bg-cyan-500/10 rounded-full border border-cyan-500/30`}>
{quest.difficulty}
</span>
<span className="text-xs text-cyan-400 font-mono flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5" />
{quest.estimatedTime}
</span>
<span className="text-xs text-green-400 font-mono font-bold flex items-center gap-1.5 bg-green-500/10 px-2 py-1 rounded-full border border-green-500/30">
<Trophy className="w-3.5 h-3.5" />
+{quest.reward} XP
</span>
</div>
</div>
<div className="flex items-center gap-2 px-4 py-2 rounded-lg bg-cyan-500/10 border border-cyan-500/30">
{quest.status === 'completed' && (
<>
<CheckCircle2 className="w-5 h-5 text-green-400" />
<span className="text-sm font-mono text-green-400 font-bold">Completed</span>
</>
)}
{quest.status === 'unlocked' && (
<>
<Zap className="w-5 h-5 text-yellow-400 animate-pulse" />
<span className="text-sm font-mono text-yellow-400 font-bold">Available</span>
</>
)}
{quest.status === 'locked' && (
<>
<Lock className="w-5 h-5 text-red-400/70" />
<span className="text-sm font-mono text-red-400/70 font-bold">Locked</span>
</>
)}
</div>
</div>
<p className="text-cyan-400/80 font-mono text-sm mb-4 leading-relaxed">
{quest.description}
</p>
{/* Prerequisites */}
{quest.prerequisites.length > 0 && (
<div className="mb-4 p-3 bg-cyan-500/5 border border-cyan-500/20 rounded-lg">
<div className="text-xs text-cyan-400/60 font-mono mb-2 flex items-center gap-2">
<Target className="w-3 h-3" />
Prerequisites:
</div>
<div className="flex flex-wrap gap-2">
{quest.prerequisites.map(prereqId => {
const prereq = quests.find(q => q.id === prereqId)
return prereq ? (
<span
key={prereqId}
className={`text-xs font-mono px-2 py-1 rounded cursor-pointer hover:scale-105 transition-transform ${
prereq.status === 'completed'
? 'bg-green-500/20 text-green-400 border border-green-500/40'
: 'bg-red-500/20 text-red-400 border border-red-500/40'
}`}
>
{prereq.name}
</span>
) : null
})}
</div>
</div>
)}
<div className="flex gap-3">
<Button
disabled={quest.status === 'locked' || quest.status === 'completed'}
className={`${
quest.status === 'unlocked'
? 'bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-400 hover:to-blue-400 text-black shadow-[0_0_30px_rgba(6,182,212,0.5)] hover:shadow-[0_0_50px_rgba(6,182,212,0.7)] cursor-pointer'
: 'bg-cyan-500/20 text-cyan-400/50 cursor-not-allowed'
} font-mono font-bold transition-all hover:scale-105 active:scale-95 flex items-center gap-2`}
>
{quest.status === 'completed' ? (
<>
<CheckCircle2 className="w-4 h-4" />
Completed
</>
) : quest.status === 'unlocked' ? (
<>
<Zap className="w-4 h-4" />
Start Mission
</>
) : (
<>
<Lock className="w-4 h-4" />
Locked
</>
)}
</Button>
{quest.status === 'completed' && (
<Button
variant="outline"
className="bg-cyan-500/10 border-cyan-500/40 text-cyan-400 hover:bg-cyan-500/20 font-mono cursor-pointer hover:scale-105 transition-transform"
>
Review
</Button>
)}
</div>
</div>
</div>
</>
)
})()}
</Card>
)}
</div>
</div>
</Card>
</div>
</main>
{/* Enhanced Background grid */}
<div
className="fixed inset-0 pointer-events-none -z-10 opacity-40"
style={{
backgroundImage: `
linear-gradient(rgba(6, 182, 212, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(6, 182, 212, 0.08) 1px, transparent 1px)
`,
backgroundSize: '60px 60px'
}}
/>
{/* Scanline effect */}
<div className="fixed inset-0 pointer-events-none z-50 opacity-5">
<div className="h-full w-full" style={{
backgroundImage: 'repeating-linear-gradient(0deg, rgba(6, 182, 212, 0.15) 0px, transparent 2px, transparent 4px)',
animation: 'scanline 8s linear infinite'
}} />
</div>
<style jsx>{`
@keyframes scanline {
0% { transform: translateY(0); }
100% { transform: translateY(100%); }
}
@keyframes moveBackground {
0% { transform: translate(0, 0); }
100% { transform: translate(40px, 40px); }
}
`}</style>
</div>
)
}