diff --git a/src/App.jsx b/src/App.jsx index b9ac263..ffc76ed 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -39,6 +39,7 @@ import TestPage from './domains/vocab/pages/TestPage' import WordListPage from './domains/vocab/pages/WordListPage' import StatsPage from './domains/vocab/pages/StatsPage' import { WritingPage } from './domains/grammar' +import { BadgeSection } from './domains/badge' import { useChat } from './contexts/ChatContext' import { useSettings } from './contexts/SettingsContext' @@ -337,10 +338,149 @@ function FreetalkAiPage() { function ReportsPage() { + const { isKorean } = useSettings() + + // 더미 통계 데이터 + const stats = { + totalStudyDays: 15, + totalWords: 285, + totalTests: 12, + averageScore: 82, + currentStreak: 5, + bestStreak: 8, + } + return ( - My Reports - Learning analytics + {/* 헤더 */} + + + + + + + + {isKorean ? '학습 리포트' : 'Learning Report'} + + + {isKorean ? '나의 학습 현황을 확인하세요' : 'Check your learning progress'} + + + + + + {/* 통계 요약 카드 */} + + + + + {isKorean ? '총 학습일' : 'Study Days'} + + + {stats.totalStudyDays} + + + {isKorean ? '일' : 'days'} + + + + + + + {isKorean ? '학습한 단어' : 'Words Learned'} + + + {stats.totalWords} + + + {isKorean ? '개' : 'words'} + + + + + + + {isKorean ? '테스트 완료' : 'Tests Taken'} + + + {stats.totalTests} + + + {isKorean ? '회' : 'tests'} + + + + + + + {isKorean ? '평균 점수' : 'Average Score'} + + + {stats.averageScore}% + + + {isKorean ? '정확도' : 'accuracy'} + + + + + + {/* 연속 학습 */} + + + {isKorean ? '연속 학습 기록' : 'Study Streak'} + + + + + + {stats.currentStreak} + + + {isKorean ? '현재 연속' : 'Current Streak'} + + + + + + + {stats.bestStreak} + + + {isKorean ? '최고 기록' : 'Best Streak'} + + + + + + + {/* 배지 섹션 */} + ) } diff --git a/src/api/badgeApi.js b/src/api/badgeApi.js new file mode 100644 index 0000000..9b05c1e --- /dev/null +++ b/src/api/badgeApi.js @@ -0,0 +1,32 @@ +import axios from 'axios' + +const badgeApi = axios.create({ + baseURL: import.meta.env.VITE_BADGE_API_URL || import.meta.env.VITE_API_URL, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}) + +// Request interceptor for JWT token +badgeApi.interceptors.request.use( + (config) => { + const token = localStorage.getItem('accessToken') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => Promise.reject(error) +) + +// Response interceptor for error handling +badgeApi.interceptors.response.use( + (response) => response.data, + (error) => { + console.error('Badge API Error:', error.response?.data || error.message) + return Promise.reject(error) + } +) + +export default badgeApi diff --git a/src/domains/badge/components/BadgeCard.jsx b/src/domains/badge/components/BadgeCard.jsx new file mode 100644 index 0000000..b4e6a38 --- /dev/null +++ b/src/domains/badge/components/BadgeCard.jsx @@ -0,0 +1,243 @@ +import { useState } from 'react' +import { + Box, + Typography, + Tooltip, + LinearProgress, + Fade, +} from '@mui/material' +import { + EmojiEvents as TrophyIcon, + Lock as LockIcon, +} from '@mui/icons-material' +import { useSettings } from '../../../contexts/SettingsContext' +import { + BADGE_NAMES_EN, + BADGE_DESCRIPTIONS_EN, + BADGE_CATEGORY_COLORS, +} from '../constants/badgeConstants' + +export default function BadgeCard({ badge, size = 'medium' }) { + const { isKorean } = useSettings() + const [imageError, setImageError] = useState(false) + + const isEarned = badge.earned + const progress = Math.min((badge.progress / badge.threshold) * 100, 100) + + // Size configurations + const sizes = { + small: { card: 100, image: 60, icon: 24 }, + medium: { card: 140, image: 80, icon: 32 }, + large: { card: 180, image: 100, icon: 40 }, + } + const config = sizes[size] || sizes.medium + + // Get localized badge info + const badgeName = isKorean ? badge.name : (BADGE_NAMES_EN[badge.badgeType] || badge.name) + const badgeDescription = isKorean + ? badge.description + : (BADGE_DESCRIPTIONS_EN[badge.badgeType] || badge.description) + + // Format earned date + const formatDate = (dateString) => { + if (!dateString) return '' + const date = new Date(dateString) + return isKorean + ? `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일` + : date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) + } + + // Tooltip content + const tooltipContent = ( + + + {badgeName} + + + {badgeDescription} + + + {!isEarned && ( + + + + {isKorean ? '진행도' : 'Progress'} + + + {badge.progress} / {badge.threshold} + + + + + )} + + {isEarned && badge.earnedAt && ( + + + + {formatDate(badge.earnedAt)} + + + )} + + ) + + return ( + + + {/* Badge Image Container */} + + {!imageError ? ( + setImageError(true)} + sx={{ + width: '70%', + height: '70%', + objectFit: 'contain', + filter: isEarned ? 'none' : 'grayscale(100%) blur(1px)', + opacity: isEarned ? 1 : 0.4, + transition: 'all 0.3s ease', + }} + /> + ) : ( + + )} + + {/* Lock overlay for unearned badges */} + {!isEarned && ( + + + + )} + + + {/* Badge Name */} + + {badgeName} + + + {/* Progress indicator for unearned */} + {!isEarned && size !== 'small' && ( + + {badge.progress}/{badge.threshold} + + )} + + + ) +} diff --git a/src/domains/badge/components/BadgeGrid.jsx b/src/domains/badge/components/BadgeGrid.jsx new file mode 100644 index 0000000..dd321b1 --- /dev/null +++ b/src/domains/badge/components/BadgeGrid.jsx @@ -0,0 +1,74 @@ +import { Box, Typography, Skeleton } from '@mui/material' +import BadgeCard from './BadgeCard' +import { useSettings } from '../../../contexts/SettingsContext' + +export default function BadgeGrid({ badges = [], loading = false, size = 'medium' }) { + const { isKorean } = useSettings() + + if (loading) { + return ( + + {Array.from({ length: 12 }).map((_, index) => ( + + + + + ))} + + ) + } + + if (badges.length === 0) { + return ( + + + {isKorean ? '배지가 없습니다' : 'No badges available'} + + + ) + } + + // Sort badges: earned first, then by category + const sortedBadges = [...badges].sort((a, b) => { + if (a.earned && !b.earned) return -1 + if (!a.earned && b.earned) return 1 + return 0 + }) + + return ( + + {sortedBadges.map((badge) => ( + + ))} + + ) +} diff --git a/src/domains/badge/components/BadgeSection.jsx b/src/domains/badge/components/BadgeSection.jsx new file mode 100644 index 0000000..3b6bc12 --- /dev/null +++ b/src/domains/badge/components/BadgeSection.jsx @@ -0,0 +1,177 @@ +import { useState, useEffect } from 'react' +import { + Box, + Typography, + Paper, + LinearProgress, + Alert, + Chip, +} from '@mui/material' +import { + EmojiEvents as TrophyIcon, + WorkspacePremium as BadgeIcon, +} from '@mui/icons-material' +import BadgeGrid from './BadgeGrid' +import { badgeService } from '../services/badgeService' +import { useSettings } from '../../../contexts/SettingsContext' + +export default function BadgeSection() { + const { isKorean } = useSettings() + const [badges, setBadges] = useState([]) + const [totalCount, setTotalCount] = useState(0) + const [earnedCount, setEarnedCount] = useState(0) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + loadBadges() + }, []) + + const loadBadges = async () => { + try { + setLoading(true) + setError(null) + const response = await badgeService.getAll() + setBadges(response.badges || []) + setTotalCount(response.totalCount || 0) + setEarnedCount(response.earnedCount || 0) + } catch (err) { + console.error('Failed to load badges:', err) + setError(isKorean ? '배지를 불러오는데 실패했습니다' : 'Failed to load badges') + } finally { + setLoading(false) + } + } + + const progress = totalCount > 0 ? (earnedCount / totalCount) * 100 : 0 + + return ( + + {/* Header */} + + + + + + + + {isKorean ? '획득한 배지' : 'Earned Badges'} + + + {isKorean + ? '학습 목표를 달성하고 배지를 모아보세요!' + : 'Achieve learning goals and collect badges!'} + + + + + {/* Stats Chip */} + } + label={ + + {earnedCount} / {totalCount} + + } + sx={{ + px: 1, + height: 36, + borderRadius: '18px', + backgroundColor: earnedCount === totalCount ? '#ecfdf5' : '#fef3c7', + color: earnedCount === totalCount ? '#059669' : '#d97706', + border: `1px solid ${earnedCount === totalCount ? '#10b981' : '#f59e0b'}`, + '& .MuiChip-icon': { + color: earnedCount === totalCount ? '#059669' : '#d97706', + }, + }} + /> + + + {/* Progress Bar */} + + + + {isKorean ? '전체 진행률' : 'Overall Progress'} + + + {Math.round(progress)}% + + + + + + {/* Error Alert */} + {error && ( + + {error} + + )} + + {/* Badge Grid */} + + + {/* Completion Message */} + {!loading && earnedCount === totalCount && totalCount > 0 && ( + + + {isKorean + ? '축하합니다! 모든 배지를 획득했습니다! 🎉' + : 'Congratulations! You collected all badges! 🎉'} + + + )} + + ) +} diff --git a/src/domains/badge/constants/badgeConstants.js b/src/domains/badge/constants/badgeConstants.js new file mode 100644 index 0000000..70c2c15 --- /dev/null +++ b/src/domains/badge/constants/badgeConstants.js @@ -0,0 +1,131 @@ +/** + * Badge domain constants + * Based on Backend BadgeType enum + */ + +// 배지 타입 +export const BADGE_TYPES = { + FIRST_STEP: 'FIRST_STEP', + STREAK_3: 'STREAK_3', + STREAK_7: 'STREAK_7', + STREAK_30: 'STREAK_30', + WORDS_100: 'WORDS_100', + WORDS_500: 'WORDS_500', + WORDS_1000: 'WORDS_1000', + PERFECT_SCORE: 'PERFECT_SCORE', + TEST_10: 'TEST_10', + ACCURACY_90: 'ACCURACY_90', + GAME_FIRST_PLAY: 'GAME_FIRST_PLAY', + GAME_10_WINS: 'GAME_10_WINS', + QUICK_GUESSER: 'QUICK_GUESSER', + PERFECT_DRAWER: 'PERFECT_DRAWER', + MASTER: 'MASTER', +} + +// 배지 카테고리 +export const BADGE_CATEGORIES = { + FIRST_STUDY: 'FIRST_STUDY', + STREAK: 'STREAK', + WORDS_LEARNED: 'WORDS_LEARNED', + PERFECT_TEST: 'PERFECT_TEST', + TESTS_COMPLETED: 'TESTS_COMPLETED', + ACCURACY: 'ACCURACY', + GAMES_PLAYED: 'GAMES_PLAYED', + GAMES_WON: 'GAMES_WON', + QUICK_GUESSES: 'QUICK_GUESSES', + PERFECT_DRAWS: 'PERFECT_DRAWS', + ALL_BADGES: 'ALL_BADGES', +} + +// 배지 카테고리 라벨 (한국어) +export const BADGE_CATEGORY_LABELS_KO = { + FIRST_STUDY: '첫 시작', + STREAK: '연속 학습', + WORDS_LEARNED: '단어 학습', + PERFECT_TEST: '완벽한 테스트', + TESTS_COMPLETED: '테스트 완료', + ACCURACY: '정확도', + GAMES_PLAYED: '게임 참여', + GAMES_WON: '게임 승리', + QUICK_GUESSES: '빠른 정답', + PERFECT_DRAWS: '완벽한 출제', + ALL_BADGES: '마스터', +} + +// 배지 카테고리 라벨 (영어) +export const BADGE_CATEGORY_LABELS_EN = { + FIRST_STUDY: 'First Steps', + STREAK: 'Streak', + WORDS_LEARNED: 'Words Learned', + PERFECT_TEST: 'Perfect Test', + TESTS_COMPLETED: 'Tests Completed', + ACCURACY: 'Accuracy', + GAMES_PLAYED: 'Games Played', + GAMES_WON: 'Games Won', + QUICK_GUESSES: 'Quick Guesser', + PERFECT_DRAWS: 'Perfect Drawer', + ALL_BADGES: 'Master', +} + +// 배지 카테고리 색상 +export const BADGE_CATEGORY_COLORS = { + FIRST_STUDY: '#10b981', + STREAK: '#f59e0b', + WORDS_LEARNED: '#3b82f6', + PERFECT_TEST: '#8b5cf6', + TESTS_COMPLETED: '#06b6d4', + ACCURACY: '#ec4899', + GAMES_PLAYED: '#14b8a6', + GAMES_WON: '#f97316', + QUICK_GUESSES: '#6366f1', + PERFECT_DRAWS: '#84cc16', + ALL_BADGES: '#eab308', +} + +// 배지 이름 (영어 - 백엔드는 한국어만 제공하므로 프론트에서 번역) +export const BADGE_NAMES_EN = { + FIRST_STEP: 'First Step', + STREAK_3: '3-Day Streak', + STREAK_7: 'Week Streak', + STREAK_30: 'Month Streak', + WORDS_100: 'Word Collector', + WORDS_500: 'Word Expert', + WORDS_1000: 'Word Master', + PERFECT_SCORE: 'Perfectionist', + TEST_10: 'Test Challenger', + ACCURACY_90: 'Accuracy Pro', + GAME_FIRST_PLAY: 'First Game', + GAME_10_WINS: '10 Wins', + QUICK_GUESSER: 'Lightning Fast', + PERFECT_DRAWER: 'Perfect Host', + MASTER: 'Learning Master', +} + +// 배지 설명 (영어) +export const BADGE_DESCRIPTIONS_EN = { + FIRST_STEP: 'Completed your first study session', + STREAK_3: 'Studied for 3 consecutive days', + STREAK_7: 'Studied for 7 consecutive days', + STREAK_30: 'Studied for 30 consecutive days', + WORDS_100: 'Learned 100 words', + WORDS_500: 'Learned 500 words', + WORDS_1000: 'Learned 1000 words', + PERFECT_SCORE: 'Got a perfect score on a test', + TEST_10: 'Completed 10 tests', + ACCURACY_90: 'Achieved 90% overall accuracy', + GAME_FIRST_PLAY: 'Played your first game', + GAME_10_WINS: 'Won 10 games', + QUICK_GUESSER: 'Answered correctly within 5 seconds', + PERFECT_DRAWER: 'All players guessed your drawing correctly', + MASTER: 'Achieved all badges', +} + +export default { + BADGE_TYPES, + BADGE_CATEGORIES, + BADGE_CATEGORY_LABELS_KO, + BADGE_CATEGORY_LABELS_EN, + BADGE_CATEGORY_COLORS, + BADGE_NAMES_EN, + BADGE_DESCRIPTIONS_EN, +} diff --git a/src/domains/badge/index.js b/src/domains/badge/index.js new file mode 100644 index 0000000..fb86312 --- /dev/null +++ b/src/domains/badge/index.js @@ -0,0 +1,10 @@ +// Components +export { default as BadgeCard } from './components/BadgeCard' +export { default as BadgeGrid } from './components/BadgeGrid' +export { default as BadgeSection } from './components/BadgeSection' + +// Services +export * from './services/badgeService' + +// Constants +export * from './constants/badgeConstants' diff --git a/src/domains/badge/services/badgeService.js b/src/domains/badge/services/badgeService.js new file mode 100644 index 0000000..e120e7b --- /dev/null +++ b/src/domains/badge/services/badgeService.js @@ -0,0 +1,220 @@ +import badgeApi from '../../../api/badgeApi' + +// Mock 데이터 사용 여부 +const USE_MOCK = true + +// Placeholder 이미지 (실제 S3 이미지가 없을 경우 대비) +const PLACEHOLDER_BADGE = 'https://via.placeholder.com/100x100/FFD700/000000?text=Badge' + +// ============================================ +// Mock 데이터 +// ============================================ + +const mockBadges = [ + { + badgeType: 'FIRST_STEP', + name: '첫 걸음', + description: '첫 학습을 완료했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3135/3135715.png', + category: 'FIRST_STUDY', + threshold: 1, + progress: 1, + earned: true, + earnedAt: '2024-12-01T10:30:00Z', + }, + { + badgeType: 'STREAK_3', + name: '3일 연속 학습', + description: '3일 연속으로 학습했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3135/3135783.png', + category: 'STREAK', + threshold: 3, + progress: 3, + earned: true, + earnedAt: '2024-12-05T09:15:00Z', + }, + { + badgeType: 'STREAK_7', + name: '일주일 연속 학습', + description: '7일 연속으로 학습했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3135/3135789.png', + category: 'STREAK', + threshold: 7, + progress: 5, + earned: false, + earnedAt: null, + }, + { + badgeType: 'STREAK_30', + name: '한 달 연속 학습', + description: '30일 연속으로 학습했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3135/3135810.png', + category: 'STREAK', + threshold: 30, + progress: 5, + earned: false, + earnedAt: null, + }, + { + badgeType: 'WORDS_100', + name: '단어 수집가', + description: '100개의 단어를 학습했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/2232/2232688.png', + category: 'WORDS_LEARNED', + threshold: 100, + progress: 85, + earned: false, + earnedAt: null, + }, + { + badgeType: 'WORDS_500', + name: '단어 전문가', + description: '500개의 단어를 학습했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/2232/2232691.png', + category: 'WORDS_LEARNED', + threshold: 500, + progress: 85, + earned: false, + earnedAt: null, + }, + { + badgeType: 'WORDS_1000', + name: '단어 마스터', + description: '1000개의 단어를 학습했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/2232/2232696.png', + category: 'WORDS_LEARNED', + threshold: 1000, + progress: 85, + earned: false, + earnedAt: null, + }, + { + badgeType: 'PERFECT_SCORE', + name: '완벽주의자', + description: '테스트에서 만점을 받았습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3176/3176293.png', + category: 'PERFECT_TEST', + threshold: 1, + progress: 1, + earned: true, + earnedAt: '2024-12-10T14:20:00Z', + }, + { + badgeType: 'TEST_10', + name: '테스트 도전자', + description: '10회의 테스트를 완료했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3176/3176366.png', + category: 'TESTS_COMPLETED', + threshold: 10, + progress: 7, + earned: false, + earnedAt: null, + }, + { + badgeType: 'ACCURACY_90', + name: '정확도 달인', + description: '전체 정확도 90%를 달성했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3176/3176271.png', + category: 'ACCURACY', + threshold: 90, + progress: 78, + earned: false, + earnedAt: null, + }, + { + badgeType: 'GAME_FIRST_PLAY', + name: '첫 게임', + description: '첫 게임에 참여했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3612/3612569.png', + category: 'GAMES_PLAYED', + threshold: 1, + progress: 1, + earned: true, + earnedAt: '2024-12-08T16:45:00Z', + }, + { + badgeType: 'GAME_10_WINS', + name: '게임 10승', + description: '게임에서 10번 1등을 했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3612/3612530.png', + category: 'GAMES_WON', + threshold: 10, + progress: 3, + earned: false, + earnedAt: null, + }, + { + badgeType: 'QUICK_GUESSER', + name: '번개 정답', + description: '5초 내에 정답을 맞췄습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3612/3612557.png', + category: 'QUICK_GUESSES', + threshold: 1, + progress: 0, + earned: false, + earnedAt: null, + }, + { + badgeType: 'PERFECT_DRAWER', + name: '완벽한 출제자', + description: '출제 시 전원이 정답을 맞췄습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3612/3612537.png', + category: 'PERFECT_DRAWS', + threshold: 1, + progress: 0, + earned: false, + earnedAt: null, + }, + { + badgeType: 'MASTER', + name: '학습 마스터', + description: '모든 업적을 달성했습니다', + imageUrl: 'https://cdn-icons-png.flaticon.com/512/3135/3135706.png', + category: 'ALL_BADGES', + threshold: 1, + progress: 0, + earned: false, + earnedAt: null, + }, +] + +// ============================================ +// API with Mock fallback +// ============================================ + +const withMock = (apiCall, mockData) => { + if (USE_MOCK) { + return new Promise((resolve) => { + setTimeout(() => resolve(mockData), 500) + }) + } + return apiCall().catch(() => mockData) +} + +/** + * Badge API Service + */ +export const badgeService = { + // GET /badges - 전체 배지 목록 (획득 여부, 진행도 포함) + getAll: () => + withMock( + () => badgeApi.get('/badges'), + { + badges: mockBadges, + totalCount: mockBadges.length, + earnedCount: mockBadges.filter((b) => b.earned).length, + } + ), + + // GET /badges/earned - 획득한 배지만 조회 + getEarned: () => + withMock( + () => badgeApi.get('/badges/earned'), + { + badges: mockBadges.filter((b) => b.earned), + count: mockBadges.filter((b) => b.earned).length, + } + ), +} + +export default badgeService diff --git a/src/domains/vocab/pages/StatsPage.jsx b/src/domains/vocab/pages/StatsPage.jsx index e2fb7a2..be57bdc 100644 --- a/src/domains/vocab/pages/StatsPage.jsx +++ b/src/domains/vocab/pages/StatsPage.jsx @@ -32,6 +32,7 @@ import { VOICE_TYPES, } from '../constants/vocabConstants' import { useTranslation } from '../../../contexts/SettingsContext' +import { BadgeSection } from '../../badge' const TEMP_USER_ID = import.meta.env.VITE_TEMP_USER_ID || 'user1' @@ -495,7 +496,7 @@ export default function StatsPage() { {/* 취약 단어 */} - + {t('stats.weakWordsTop10')} @@ -514,6 +515,9 @@ export default function StatsPage() { playingWordId={playingWordId} /> + + {/* 배지 섹션 */} + ) } diff --git a/src/i18n/translations.js b/src/i18n/translations.js index e96c160..15b5bcf 100644 --- a/src/i18n/translations.js +++ b/src/i18n/translations.js @@ -349,6 +349,32 @@ export const translations = { poor: '더 연습해요', }, }, + + // Badge + badge: { + title: '획득한 배지', + subtitle: '학습 목표를 달성하고 배지를 모아보세요!', + progress: '전체 진행률', + earned: '획득', + notEarned: '미획득', + earnedAt: '획득일', + progressLabel: '진행도', + allCollected: '축하합니다! 모든 배지를 획득했습니다!', + noBadges: '배지가 없습니다', + categories: { + FIRST_STUDY: '첫 시작', + STREAK: '연속 학습', + WORDS_LEARNED: '단어 학습', + PERFECT_TEST: '완벽한 테스트', + TESTS_COMPLETED: '테스트 완료', + ACCURACY: '정확도', + GAMES_PLAYED: '게임 참여', + GAMES_WON: '게임 승리', + QUICK_GUESSES: '빠른 정답', + PERFECT_DRAWS: '완벽한 출제', + ALL_BADGES: '마스터', + }, + }, }, en: { @@ -696,6 +722,32 @@ export const translations = { poor: 'Keep practicing', }, }, + + // Badge + badge: { + title: 'Earned Badges', + subtitle: 'Achieve learning goals and collect badges!', + progress: 'Overall Progress', + earned: 'Earned', + notEarned: 'Not earned', + earnedAt: 'Earned on', + progressLabel: 'Progress', + allCollected: 'Congratulations! You collected all badges!', + noBadges: 'No badges available', + categories: { + FIRST_STUDY: 'First Steps', + STREAK: 'Streak', + WORDS_LEARNED: 'Words Learned', + PERFECT_TEST: 'Perfect Test', + TESTS_COMPLETED: 'Tests Completed', + ACCURACY: 'Accuracy', + GAMES_PLAYED: 'Games Played', + GAMES_WON: 'Games Won', + QUICK_GUESSES: 'Quick Guesser', + PERFECT_DRAWS: 'Perfect Drawer', + ALL_BADGES: 'Master', + }, + }, }, }