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
25 changes: 25 additions & 0 deletions README_ACHIEVEMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Achievements & Tournaments

## Achievements

Unlock achievements by completing challenges and milestones.

### Categories
- **Milestone**: Major accomplishments
- **Streak**: Consecutive day bonuses
- **Time**: Speed-based achievements
- **Skill**: Accuracy and knowledge

## Tournaments

Compete in weekly and special tournaments for bigger prizes.

### Status Types
- **Upcoming**: Registration open
- **Active**: Tournament in progress
- **Completed**: Results finalized

### Features
- Prize pools
- Participant tracking
- Multiple tournament types
4 changes: 4 additions & 0 deletions frontend/src/app/achievements/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ReactNode } from 'react';
export default function AchievementsLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
4 changes: 4 additions & 0 deletions frontend/src/app/tournaments/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ReactNode } from 'react';
export default function TournamentsLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
26 changes: 26 additions & 0 deletions frontend/src/app/tournaments/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client';

import React from 'react';
import { TournamentCard } from '@/components/tournaments';
import { useTournaments } from '@/hooks/useTournaments';

export default function TournamentsPage() {
const { tournaments, isLoading } = useTournaments();

if (isLoading) {
return <div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">Loading...</div>;
}

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">Tournaments</h1>
<div className="grid gap-6">
{tournaments.map((t) => (
<TournamentCard key={t.id} tournament={t} />
))}
</div>
</div>
</div>
);
}
37 changes: 37 additions & 0 deletions frontend/src/components/achievements/AchievementBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';

interface AchievementBadgeProps {
icon: string;
name: string;
description: string;
isUnlocked: boolean;
progress?: number;
target?: number;
}

export const AchievementBadge: React.FC<AchievementBadgeProps> = ({
icon,
name,
description,
isUnlocked,
progress,
target,
}) => {
const progressPercent = progress && target ? (progress / target) * 100 : 0;

return (
<div className={`p-4 rounded-xl border-2 ${isUnlocked ? 'border-yellow-500 bg-yellow-50 dark:bg-yellow-900/20' : 'border-gray-200 dark:border-gray-700'}`}>
<div className="text-4xl mb-2">{isUnlocked ? icon : '🔒'}</div>
<h3 className="font-bold text-gray-900 dark:text-white">{name}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">{description}</p>
{!isUnlocked && progress !== undefined && target && (
<div className="mt-3">
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div className="bg-yellow-500 h-2 rounded-full" style={{ width: `${progressPercent}%` }} />
</div>
<p className="text-xs text-gray-500 mt-1">{progress}/{target}</p>
</div>
)}
</div>
);
};
1 change: 1 addition & 0 deletions frontend/src/components/achievements/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AchievementBadge } from './AchievementBadge';
32 changes: 32 additions & 0 deletions frontend/src/components/rewards/RewardProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { REWARD_TIERS } from '@/constants/rewards';

interface RewardProgressProps {
currentPoints: number;
}

export const RewardProgress: React.FC<RewardProgressProps> = ({ currentPoints }) => {
const currentTier = REWARD_TIERS.slice().reverse().find(t => currentPoints >= t.minPoints) || REWARD_TIERS[0];
const nextTier = REWARD_TIERS.find(t => t.minPoints > currentPoints);
const progress = nextTier
? ((currentPoints - currentTier.minPoints) / (nextTier.minPoints - currentTier.minPoints)) * 100
: 100;

return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg">
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">Your Rewards</h3>
<div className="mb-4">
<p className="text-3xl font-bold text-purple-600">{currentPoints} pts</p>
<p className="text-gray-600 dark:text-gray-400">{currentTier.name} Tier</p>
</div>
{nextTier && (
<div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
<div className="bg-purple-600 h-2 rounded-full" style={{ width: `${progress}%` }} />
</div>
<p className="text-sm text-gray-500">{nextTier.minPoints - currentPoints} points to {nextTier.name}</p>
</div>
)}
</div>
);
};
1 change: 1 addition & 0 deletions frontend/src/components/rewards/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RewardProgress } from './RewardProgress';
37 changes: 37 additions & 0 deletions frontend/src/components/tournaments/TournamentCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { Tournament } from '@/types/tournament';
import { formatTournamentStatus } from '@/constants/tournament';

interface TournamentCardProps {
tournament: Tournament;
onJoin?: (id: string) => void;
}

export const TournamentCard: React.FC<TournamentCardProps> = ({ tournament, onJoin }) => {
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg border-2 border-purple-500">
<div className="flex justify-between items-start mb-4">
<h3 className="text-xl font-bold text-gray-900 dark:text-white">{tournament.name}</h3>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${formatTournamentStatus(tournament.status)}`}>
{tournament.status}
</span>
</div>
<p className="text-gray-600 dark:text-gray-400 mb-4">{tournament.description}</p>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-sm text-gray-500">Prize Pool</p>
<p className="text-xl font-bold text-purple-600">{tournament.prizePool}</p>
</div>
<div>
<p className="text-sm text-gray-500">Participants</p>
<p className="text-xl font-bold">{tournament.participants}/{tournament.maxParticipants}</p>
</div>
</div>
{tournament.status === 'upcoming' && onJoin && (
<button onClick={() => onJoin(tournament.id)} className="w-full py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium">
Join Tournament
</button>
)}
</div>
);
};
1 change: 1 addition & 0 deletions frontend/src/components/tournaments/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TournamentCard } from './TournamentCard';
13 changes: 13 additions & 0 deletions frontend/src/constants/rewards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface RewardTier {
tier: number;
name: string;
minPoints: number;
rewards: string[];
}

export const REWARD_TIERS: RewardTier[] = [
{ tier: 1, name: 'Bronze', minPoints: 0, rewards: ['Bronze Badge', '5% bonus'] },
{ tier: 2, name: 'Silver', minPoints: 5000, rewards: ['Silver Badge', '10% bonus', 'Early access'] },
{ tier: 3, name: 'Gold', minPoints: 15000, rewards: ['Gold Badge', '15% bonus', 'Exclusive events'] },
{ tier: 4, name: 'Platinum', minPoints: 50000, rewards: ['Platinum Badge', '20% bonus', 'VIP support'] },
];
16 changes: 16 additions & 0 deletions frontend/src/constants/tournament.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Tournament } from '@/types/tournament';

export const TOURNAMENT_CONFIG = {
MIN_PARTICIPANTS: 10,
MAX_PARTICIPANTS: 1000,
DURATION_DAYS: 7,
};

export const formatTournamentStatus = (status: string) => {
const colors = {
upcoming: 'bg-blue-100 text-blue-700',
active: 'bg-green-100 text-green-700',
completed: 'bg-gray-100 text-gray-700',
};
return colors[status as keyof typeof colors] || '';
};
23 changes: 23 additions & 0 deletions frontend/src/hooks/useAchievements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react';
import { Achievement, AchievementState } from '@/types/achievement';

export const useAchievements = () => {
const [achievements, setAchievements] = useState<Achievement[]>([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const fetchAchievements = async () => {
await new Promise((r) => setTimeout(r, 300));
setAchievements([
{ id: '1', title: 'First Win', description: 'Win your first game', icon: '🎯', category: 'milestone', unlockedAt: '2025-01-15', progress: 1, target: 1, isUnlocked: true },
{ id: '2', title: 'Streak Master', description: 'Get a 7-day streak', icon: '🔥', category: 'streak', progress: 5, target: 7, isUnlocked: false },
{ id: '3', title: 'Speed Demon', description: 'Answer 50 questions in under 10s', icon: '⚡', category: 'time', progress: 35, target: 50, isUnlocked: false },
{ id: '4', title: 'Trivia Expert', description: 'Answer 500 questions correctly', icon: '🧠', category: 'skill', progress: 420, target: 500, isUnlocked: false },
]);
setIsLoading(false);
};
fetchAchievements();
}, []);

return { achievements, isLoading };
};
21 changes: 21 additions & 0 deletions frontend/src/hooks/useTournaments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useState, useEffect } from 'react';
import { Tournament } from '@/types/tournament';

export const useTournaments = () => {
const [tournaments, setTournaments] = useState<Tournament[]>([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const fetchTournaments = async () => {
await new Promise((r) => setTimeout(r, 300));
setTournaments([
{ id: '1', name: 'Weekly Championship', description: 'Compete for the top spot', startDate: new Date(), endDate: new Date(Date.now() + 7 * 86400000), prizePool: 10000, participants: 150, maxParticipants: 500, status: 'active' },
{ id: '2', name: 'Weekend Warrior', description: 'Special weekend tournament', startDate: new Date(Date.now() + 3 * 86400000), endDate: new Date(Date.now() + 5 * 86400000), prizePool: 5000, participants: 0, maxParticipants: 200, status: 'upcoming' },
]);
setIsLoading(false);
};
fetchTournaments();
}, []);

return { tournaments, isLoading };
};
11 changes: 11 additions & 0 deletions frontend/src/types/tournament.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface Tournament {
id: string;
name: string;
description: string;
startDate: Date;
endDate: Date;
prizePool: number;
participants: number;
maxParticipants: number;
status: 'upcoming' | 'active' | 'completed';
}
Loading