Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README_DIFFICULTY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Difficulty Levels

Four difficulty levels with increasing challenges and rewards.

## Levels

| Level | Multiplier | Time | Unlock |
|-------|------------|------|--------|
| Easy 🌱 | 1.0x | 60s | Default |
| Medium ⚡ | 1.5x | 45s | 1000 pts |
| Hard 🔥 | 2.0x | 30s | 5000 pts |
| Expert 💎 | 3.0x | 20s | 15000 pts |

## Features

- Progressive difficulty unlock system
- Point multipliers for harder difficulties
- Visual indicators for locked levels
- Time-based challenges
4 changes: 4 additions & 0 deletions frontend/src/app/difficulty/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ReactNode } from 'react';
export default function DifficultyLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
24 changes: 24 additions & 0 deletions frontend/src/app/difficulty/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import React from 'react';
import { DifficultySelector } from '@/components/difficulty';
import { useDifficulty } from '@/hooks/useDifficulty';

export default function DifficultyPage() {
const { selectedDifficulty, unlockedDifficulties, selectDifficulty } = useDifficulty();

return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8 px-4">
<div className="max-w-4xl mx-auto">
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-8">
Select Difficulty
</h1>
<DifficultySelector
selected={selectedDifficulty}
onSelect={selectDifficulty}
unlockedLevels={unlockedDifficulties}
/>
</div>
</div>
);
}
37 changes: 37 additions & 0 deletions frontend/src/components/difficulty/DifficultyCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { DifficultyLevel, DifficultyConfig, DIFFICULTY_LEVELS } from '@/constants/difficulty';

interface DifficultyCardProps {
difficulty: DifficultyConfig;
isSelected: boolean;
isUnlocked: boolean;
onSelect: () => void;
}

export const DifficultyCard: React.FC<DifficultyCardProps> = ({
difficulty,
isSelected,
isUnlocked,
onSelect,
}) => {
return (
<button
onClick={isUnlocked ? onSelect : undefined}
disabled={!isUnlocked}
className={`p-6 rounded-xl border-2 transition-all ${
isSelected ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-200 dark:border-gray-700'
} ${!isUnlocked ? 'opacity-50 cursor-not-allowed' : 'hover:border-purple-300'}`}
>
<div className="text-4xl mb-3">{difficulty.icon}</div>
<h3 className="font-bold text-gray-900 dark:text-white mb-1">{difficulty.name}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{difficulty.description}</p>
<div className="flex items-center justify-between">
<span className="text-lg font-bold" style={{ color: difficulty.color }}>
{difficulty.pointMultiplier}x
</span>
{!isUnlocked && (
<span className="text-xs text-gray-500">🔒 {difficulty.unlockRequirement} pts</span>
)}
</div>
</button>
);
};
29 changes: 29 additions & 0 deletions frontend/src/components/difficulty/DifficultySelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { DifficultyCard } from './DifficultyCard';
import { DifficultyLevel, DIFFICULTY_LEVELS } from '@/constants/difficulty';

interface DifficultySelectorProps {
selected: DifficultyLevel;
onSelect: (level: DifficultyLevel) => void;
unlockedLevels: DifficultyLevel[];
}

export const DifficultySelector: React.FC<DifficultySelectorProps> = ({
selected,
onSelect,
unlockedLevels,
}) => {
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{DIFFICULTY_LEVELS.map((difficulty) => (
<DifficultyCard
key={difficulty.level}
difficulty={difficulty}
isSelected={selected === difficulty.level}
isUnlocked={unlockedLevels.includes(difficulty.level)}
onSelect={() => onSelect(difficulty.level)}
/>
))}
</div>
);
};
2 changes: 2 additions & 0 deletions frontend/src/components/difficulty/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DifficultyCard } from './DifficultyCard';
export { DifficultySelector } from './DifficultySelector';
37 changes: 37 additions & 0 deletions frontend/src/components/leaderboard/LeaderboardList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { LeaderboardEntry } from '@/types/leaderboard';

interface LeaderboardListProps {
entries: LeaderboardEntry[];
}

export const LeaderboardList: React.FC<LeaderboardListProps> = ({ entries }) => {
return (
<div className="space-y-2">
{entries.map((entry) => (
<div
key={entry.rank}
className={`flex items-center justify-between p-4 rounded-lg ${
entry.rank <= 3
? 'bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20'
: 'bg-gray-50 dark:bg-gray-800'
}`}
>
<div className="flex items-center gap-4">
<span className="text-2xl w-10 text-center">
{entry.rank === 1 ? '🥇' : entry.rank === 2 ? '🥈' : entry.rank === 3 ? '🥉' : `#${entry.rank}`}
</span>
<div>
<p className="font-semibold text-gray-900 dark:text-white">{entry.username}</p>
<p className="text-sm text-gray-500">{entry.address}</p>
</div>
</div>
<div className="text-right">
<p className="text-xl font-bold text-purple-600">{entry.score}</p>
<p className="text-sm text-gray-500">{entry.gamesPlayed} games</p>
</div>
</div>
))}
</div>
);
};
1 change: 1 addition & 0 deletions frontend/src/components/leaderboard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { LeaderboardList } from './LeaderboardList';
62 changes: 62 additions & 0 deletions frontend/src/constants/difficulty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
export type DifficultyLevel = 'easy' | 'medium' | 'hard' | 'expert';

export interface DifficultyConfig {
level: DifficultyLevel;
name: string;
description: string;
icon: string;
color: string;
pointMultiplier: number;
timeLimit: number;
unlockRequirement?: number;
}

export const DIFFICULTY_LEVELS: DifficultyConfig[] = [
{
level: 'easy',
name: 'Easy',
description: 'Perfect for beginners',
icon: '🌱',
color: '#10B981',
pointMultiplier: 1.0,
timeLimit: 60,
},
{
level: 'medium',
name: 'Medium',
description: 'A balanced challenge',
icon: '⚡',
color: '#F59E0B',
pointMultiplier: 1.5,
timeLimit: 45,
unlockRequirement: 1000,
},
{
level: 'hard',
name: 'Hard',
description: 'Test your knowledge',
icon: '🔥',
color: '#EF4444',
pointMultiplier: 2.0,
timeLimit: 30,
unlockRequirement: 5000,
},
{
level: 'expert',
name: 'Expert',
description: 'Only for masters',
icon: '💎',
color: '#8B5CF6',
pointMultiplier: 3.0,
timeLimit: 20,
unlockRequirement: 15000,
},
];

export const getDifficultyByLevel = (level: DifficultyLevel) =>
DIFFICULTY_LEVELS.find(d => d.level === level);

export const calculatePoints = (basePoints: number, difficulty: DifficultyLevel) => {
const config = getDifficultyByLevel(difficulty);
return Math.round(basePoints * (config?.pointMultiplier || 1));
};
29 changes: 29 additions & 0 deletions frontend/src/hooks/useDifficulty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useState, useCallback, useEffect } from 'react';
import { DifficultyLevel } from '@/constants/difficulty';

export const useDifficulty = (initialLevel: DifficultyLevel = 'easy') => {
const [selectedDifficulty, setSelectedDifficulty] = useState<DifficultyLevel>(initialLevel);
const [unlockedDifficulties, setUnlockedDifficulties] = useState<DifficultyLevel[]>(['easy']);

const unlockDifficulty = useCallback((level: DifficultyLevel) => {
setUnlockedDifficulties((prev) => {
if (!prev.includes(level)) {
return [...prev, level];
}
return prev;
});
}, []);

const selectDifficulty = useCallback((level: DifficultyLevel) => {
if (unlockedDifficulties.includes(level)) {
setSelectedDifficulty(level);
}
}, [unlockedDifficulties]);

return {
selectedDifficulty,
unlockedDifficulties,
selectDifficulty,
unlockDifficulty,
};
};
26 changes: 26 additions & 0 deletions frontend/src/hooks/useGameStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useState, useEffect } from 'react';
import { GameStats } from '@/types/stats';

export const useGameStats = () => {
const [stats, setStats] = useState<GameStats | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const fetchStats = async () => {
await new Promise((r) => setTimeout(r, 300));
setStats({
totalGamesPlayed: 50,
totalQuestionsAnswered: 500,
correctAnswers: 420,
totalPointsEarned: 25000,
highestScore: 9500,
currentStreak: 7,
longestStreak: 15,
});
setIsLoading(false);
};
fetchStats();
}, []);

return { stats, isLoading };
};
25 changes: 25 additions & 0 deletions frontend/src/hooks/useLeaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useState, useEffect } from 'react';
import { LeaderboardEntry } from '@/types/leaderboard';

export const useLeaderboard = (limit = 10) => {
const [entries, setEntries] = useState<LeaderboardEntry[]>([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const fetchLeaderboard = async () => {
await new Promise((r) => setTimeout(r, 300));
const mockEntries: LeaderboardEntry[] = Array.from({ length: limit }, (_, i) => ({
rank: i + 1,
address: `0x${(i * 1111).toString(16)}...${(i * 9999).toString(16).slice(-4)}`,
username: `Player${i + 1}`,
score: 10000 - i * 500,
gamesPlayed: 50 - i * 2,
}));
setEntries(mockEntries);
setIsLoading(false);
};
fetchLeaderboard();
}, [limit]);

return { entries, isLoading };
};
7 changes: 7 additions & 0 deletions frontend/src/types/leaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface LeaderboardEntry {
rank: number;
address: string;
username: string;
score: number;
gamesPlayed: number;
}
9 changes: 9 additions & 0 deletions frontend/src/types/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface GameStats {
totalGamesPlayed: number;
totalQuestionsAnswered: number;
correctAnswers: number;
totalPointsEarned: number;
highestScore: number;
currentStreak: number;
longestStreak: number;
}
11 changes: 11 additions & 0 deletions frontend/src/utils/statsUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const calculateAccuracy = (correct: number, total: number): number => {
if (total === 0) return 0;
return Math.round((correct / total) * 100);
};

export const formatPoints = (points: number): string => {
if (points >= 1000) {
return `${(points / 1000).toFixed(1)}k`;
}
return points.toString();
};
Loading