initial commit (backend)
This commit is contained in:
@@ -0,0 +1,530 @@
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from auth import require_auth
|
||||
from database import get_pool
|
||||
|
||||
router = APIRouter(prefix="/achievements", tags=["achievements"])
|
||||
|
||||
# ============ Данные 52 достижений ============
|
||||
ACHIEVEMENTS_DATA = [
|
||||
# ===== Word Search (11) =====
|
||||
{"code": "first_level", "category": "wordsearch", "icon": "🔰",
|
||||
"name_pl": "Pierwszy krok", "name_ru": "Первый шаг", "name_uk": "Перший крок", "name_en": "First Step",
|
||||
"desc_pl": "Ukończ 1 poziom filwordów", "desc_ru": "Пройти 1 уровень филвордов", "desc_uk": "Пройти 1 рівень філвордів", "desc_en": "Complete 1 word search level",
|
||||
"type": "levels_completed", "value": 1},
|
||||
|
||||
{"code": "ten_levels", "category": "wordsearch", "icon": "⭐",
|
||||
"name_pl": "Gracz", "name_ru": "Игрок", "name_uk": "Гравець", "name_en": "Player",
|
||||
"desc_pl": "Ukończ 10 poziomów filwordów", "desc_ru": "Пройти 10 уровней филвордов", "desc_uk": "Пройти 10 рівнів філвордів", "desc_en": "Complete 10 word search levels",
|
||||
"type": "levels_completed", "value": 10},
|
||||
|
||||
{"code": "fifty_levels", "category": "wordsearch", "icon": "🏅",
|
||||
"name_pl": "Ekspert", "name_ru": "Эксперт", "name_uk": "Експерт", "name_en": "Expert",
|
||||
"desc_pl": "Ukończ 50 poziomów filwordów", "desc_ru": "Пройти 50 уровней филвордов", "desc_uk": "Пройти 50 рівнів філвордів", "desc_en": "Complete 50 word search levels",
|
||||
"type": "levels_completed", "value": 50},
|
||||
|
||||
{"code": "hundred_levels", "category": "wordsearch", "icon": "👑",
|
||||
"name_pl": "Król", "name_ru": "Король", "name_uk": "Король", "name_en": "King",
|
||||
"desc_pl": "Ukończ 100 poziomów filwordów", "desc_ru": "Пройти 100 уровней филвордов", "desc_uk": "Пройти 100 рівнів філвордів", "desc_en": "Complete 100 word search levels",
|
||||
"type": "levels_completed", "value": 100},
|
||||
|
||||
{"code": "ws_200", "category": "wordsearch", "icon": "🎯",
|
||||
"name_pl": "Mistrz", "name_ru": "Мастер", "name_uk": "Майстер", "name_en": "Master",
|
||||
"desc_pl": "Ukończ 200 poziomów filwordów", "desc_ru": "Пройти 200 уровней филвордов", "desc_uk": "Пройти 200 рівнів філвордів", "desc_en": "Complete 200 word search levels",
|
||||
"type": "levels_completed", "value": 200},
|
||||
|
||||
{"code": "ws_500", "category": "wordsearch", "icon": "🌟",
|
||||
"name_pl": "Legenda filwordów", "name_ru": "Легенда филвордов", "name_uk": "Легенда філвордів", "name_en": "Word Search Legend",
|
||||
"desc_pl": "Ukończ 500 poziomów filwordów", "desc_ru": "Пройти 500 уровней филвордов", "desc_uk": "Пройти 500 рівнів філвордів", "desc_en": "Complete 500 word search levels",
|
||||
"type": "levels_completed", "value": 500},
|
||||
|
||||
{"code": "no_hints", "category": "wordsearch", "icon": "💡",
|
||||
"name_pl": "Bystrzak", "name_ru": "Умница", "name_uk": "Розумник", "name_en": "Smart",
|
||||
"desc_pl": "Ukończ poziom bez podpowiedzi", "desc_ru": "Пройти уровень без подсказок", "desc_uk": "Пройти рівень без підказок", "desc_en": "Complete a level without hints",
|
||||
"type": "no_hints_level", "value": 1},
|
||||
|
||||
{"code": "streak_5", "category": "wordsearch", "icon": "🔥",
|
||||
"name_pl": "Seria", "name_ru": "Серия", "name_uk": "Серія", "name_en": "Streak",
|
||||
"desc_pl": "Ukończ 5 poziomów z rzędu", "desc_ru": "Пройти 5 уровней подряд", "desc_uk": "Пройти 5 рівнів поспіль", "desc_en": "Complete 5 levels in a row",
|
||||
"type": "level_streak", "value": 5},
|
||||
|
||||
{"code": "speed_60", "category": "wordsearch", "icon": "⚡",
|
||||
"name_pl": "Szybki", "name_ru": "Быстрый", "name_uk": "Швидкий", "name_en": "Fast",
|
||||
"desc_pl": "Ukończ poziom w mniej niż 60 sekund", "desc_ru": "Пройти уровень быстрее 60 секунд", "desc_uk": "Пройти рівень швидше 60 секунд", "desc_en": "Complete a level in under 60 seconds",
|
||||
"type": "speed_level", "value": 60},
|
||||
|
||||
{"code": "ws_speed_30", "category": "wordsearch", "icon": "🚀",
|
||||
"name_pl": "Błyskawica", "name_ru": "Молния", "name_uk": "Блискавка", "name_en": "Lightning",
|
||||
"desc_pl": "Ukończ poziom w mniej niż 30 sekund", "desc_ru": "Пройти уровень быстрее 30 секунд", "desc_uk": "Пройти рівень швидше 30 секунд", "desc_en": "Complete a level in under 30 seconds",
|
||||
"type": "speed_level", "value": 30},
|
||||
|
||||
{"code": "perfect_5", "category": "wordsearch", "icon": "🎯",
|
||||
"name_pl": "Snajper", "name_ru": "Снайпер", "name_uk": "Снайпер", "name_en": "Sniper",
|
||||
"desc_pl": "Znajdź 5 słów bez błędu", "desc_ru": "Найти 5 слов подряд без ошибок", "desc_uk": "Знайти 5 слів без помилки", "desc_en": "Find 5 words without mistakes",
|
||||
"type": "perfect_words", "value": 5},
|
||||
|
||||
# ===== Sentence Builder (10) =====
|
||||
{"code": "first_sentence", "category": "sentence", "icon": "📝",
|
||||
"name_pl": "Debiutant", "name_ru": "Дебютант", "name_uk": "Дебютант", "name_en": "Debutant",
|
||||
"desc_pl": "Ułóż 1 zdanie", "desc_ru": "Составить 1 предложение", "desc_uk": "Скласти 1 речення", "desc_en": "Build 1 sentence",
|
||||
"type": "sentences_completed", "value": 1},
|
||||
|
||||
{"code": "ten_sentences", "category": "sentence", "icon": "✍️",
|
||||
"name_pl": "Autor", "name_ru": "Автор", "name_uk": "Автор", "name_en": "Author",
|
||||
"desc_pl": "Ułóż 10 zdań", "desc_ru": "Составить 10 предложений", "desc_uk": "Скласти 10 речень", "desc_en": "Build 10 sentences",
|
||||
"type": "sentences_completed", "value": 10},
|
||||
|
||||
{"code": "fifty_sentences", "category": "sentence", "icon": "🎓",
|
||||
"name_pl": "Profesor", "name_ru": "Профессор", "name_uk": "Професор", "name_en": "Professor",
|
||||
"desc_pl": "Ułóż 50 zdań", "desc_ru": "Составить 50 предложений", "desc_uk": "Скласти 50 речень", "desc_en": "Build 50 sentences",
|
||||
"type": "sentences_completed", "value": 50},
|
||||
|
||||
{"code": "hundred_sentences", "category": "sentence", "icon": "📚",
|
||||
"name_pl": "Bibliotekarz", "name_ru": "Библиотекарь", "name_uk": "Бібліотекар", "name_en": "Librarian",
|
||||
"desc_pl": "Ułóż 100 zdań", "desc_ru": "Составить 100 предложений", "desc_uk": "Скласти 100 речень", "desc_en": "Build 100 sentences",
|
||||
"type": "sentences_completed", "value": 100},
|
||||
|
||||
{"code": "sentence_200", "category": "sentence", "icon": "📖",
|
||||
"name_pl": "Pisarz", "name_ru": "Писатель", "name_uk": "Письменник", "name_en": "Writer",
|
||||
"desc_pl": "Ułóż 200 zdań", "desc_ru": "Составить 200 предложений", "desc_uk": "Скласти 200 речень", "desc_en": "Build 200 sentences",
|
||||
"type": "sentences_completed", "value": 200},
|
||||
|
||||
{"code": "perfect_sentence_5", "category": "sentence", "icon": "✨",
|
||||
"name_pl": "Perfekcjonista", "name_ru": "Перфекционист", "name_uk": "Перфекціоніст", "name_en": "Perfectionist",
|
||||
"desc_pl": "Ułóż 5 zdań bez błędu", "desc_ru": "Составить 5 предложений без единой ошибки", "desc_uk": "Скласти 5 речень без жодної помилки", "desc_en": "Build 5 sentences without mistakes",
|
||||
"type": "perfect_sentences", "value": 5},
|
||||
|
||||
{"code": "sentence_perfect_10", "category": "sentence", "icon": "💎",
|
||||
"name_pl": "Mistrz zdań", "name_ru": "Мастер предложений", "name_uk": "Майстер речень", "name_en": "Sentence Master",
|
||||
"desc_pl": "Ułóż 10 zdań bez błędu", "desc_ru": "Составить 10 предложений без ошибок", "desc_uk": "Скласти 10 речень без помилок", "desc_en": "Build 10 sentences without mistakes",
|
||||
"type": "perfect_sentences", "value": 10},
|
||||
|
||||
{"code": "sentence_streak_10", "category": "sentence", "icon": "💪",
|
||||
"name_pl": "Nieomylny", "name_ru": "Безошибочный", "name_uk": "Безпомилковий", "name_en": "Flawless",
|
||||
"desc_pl": "Ułóż 10 zdań z rzędu bez błędu", "desc_ru": "Составить 10 предложений подряд без ошибок", "desc_uk": "Скласти 10 речень поспіль без помилок", "desc_en": "Build 10 sentences in a row without mistakes",
|
||||
"type": "sentence_streak", "value": 10},
|
||||
|
||||
{"code": "speed_10", "category": "sentence", "icon": "🚀",
|
||||
"name_pl": "Turbo", "name_ru": "Турбо", "name_uk": "Турбо", "name_en": "Turbo",
|
||||
"desc_pl": "Ułóż zdanie w mniej niż 10 sekund", "desc_ru": "Составить предложение быстрее 10 секунд", "desc_uk": "Скласти речення швидше 10 секунд", "desc_en": "Build a sentence in under 10 seconds",
|
||||
"type": "speed_sentence", "value": 10},
|
||||
|
||||
{"code": "sentence_speed_5", "category": "sentence", "icon": "⚡",
|
||||
"name_pl": "Ekspres", "name_ru": "Экспресс", "name_uk": "Експрес", "name_en": "Express",
|
||||
"desc_pl": "Ułóż zdanie w mniej niż 5 sekund", "desc_ru": "Составить предложение быстрее 5 секунд", "desc_uk": "Скласти речення швидше 5 секунд", "desc_en": "Build a sentence in under 5 seconds",
|
||||
"type": "speed_sentence", "value": 5},
|
||||
|
||||
# ===== Word Quiz (8) =====
|
||||
{"code": "quiz_10", "category": "wordquiz", "icon": "🧠",
|
||||
"name_pl": "Myśliciel", "name_ru": "Мыслитель", "name_uk": "Мислитель", "name_en": "Thinker",
|
||||
"desc_pl": "Odgadnij 10 słów w Quiz słownym", "desc_ru": "Угадать 10 слов в Квизе слов", "desc_uk": "Відгадати 10 слів у Квізі слів", "desc_en": "Guess 10 words in Word Quiz",
|
||||
"type": "quiz_guessed", "value": 10},
|
||||
|
||||
{"code": "quiz_50", "category": "wordquiz", "icon": "🎓",
|
||||
"name_pl": "Mistrz quizu", "name_ru": "Мастер квиза", "name_uk": "Майстер квізу", "name_en": "Quiz Master",
|
||||
"desc_pl": "Odgadnij 50 słów w Quiz słownym", "desc_ru": "Угадать 50 слов в Квизе слов", "desc_uk": "Відгадати 50 слів у Квізі слів", "desc_en": "Guess 50 words in Word Quiz",
|
||||
"type": "quiz_guessed", "value": 50},
|
||||
|
||||
{"code": "quiz_100", "category": "wordquiz", "icon": "🏆",
|
||||
"name_pl": "Mózg", "name_ru": "Мозг", "name_uk": "Мозок", "name_en": "Brain",
|
||||
"desc_pl": "Odgadnij 100 słów w Quiz słownym", "desc_ru": "Угадать 100 слов в Квизе слов", "desc_uk": "Відгадати 100 слів у Квізі слів", "desc_en": "Guess 100 words in Word Quiz",
|
||||
"type": "quiz_guessed", "value": 100},
|
||||
|
||||
{"code": "quiz_perfect_10", "category": "wordquiz", "icon": "💎",
|
||||
"name_pl": "Idealny wynik", "name_ru": "Идеальный результат", "name_uk": "Ідеальний результат", "name_en": "Perfect Score",
|
||||
"desc_pl": "Odgadnij 10 słów bez pomyłki", "desc_ru": "Угадать 10 слов без ошибок", "desc_uk": "Відгадати 10 слів без помилок", "desc_en": "Guess 10 words without mistakes",
|
||||
"type": "quiz_perfect_streak", "value": 10},
|
||||
|
||||
{"code": "quiz_streak_20", "category": "wordquiz", "icon": "🔥",
|
||||
"name_pl": "Niepowstrzymany", "name_ru": "Неудержимый", "name_uk": "Невтримний", "name_en": "Unstoppable",
|
||||
"desc_pl": "Odgadnij 20 słów z rzędu", "desc_ru": "Угадать 20 слов подряд", "desc_uk": "Відгадати 20 слів поспіль", "desc_en": "Guess 20 words in a row",
|
||||
"type": "quiz_perfect_streak", "value": 20},
|
||||
|
||||
{"code": "quiz_speed", "category": "wordquiz", "icon": "⚡",
|
||||
"name_pl": "Błyskawica", "name_ru": "Молния", "name_uk": "Блискавка", "name_en": "Lightning",
|
||||
"desc_pl": "Odgadnij słowo w mniej niż 3 sekundy", "desc_ru": "Угадать слово быстрее 3 секунд", "desc_uk": "Відгадати слово швидше 3 секунд", "desc_en": "Guess a word in under 3 seconds",
|
||||
"type": "quiz_speed", "value": 3},
|
||||
|
||||
{"code": "quiz_speed_2", "category": "wordquiz", "icon": "🚀",
|
||||
"name_pl": "Super szybki", "name_ru": "Супер быстрый", "name_uk": "Супер швидкий", "name_en": "Super Fast",
|
||||
"desc_pl": "Odgadnij słowo w mniej niż 2 sekundy", "desc_ru": "Угадать слово быстрее 2 секунд", "desc_uk": "Відгадати слово швидше 2 секунд", "desc_en": "Guess a word in under 2 seconds",
|
||||
"type": "quiz_speed", "value": 2},
|
||||
|
||||
{"code": "quiz_no_mistakes", "category": "wordquiz", "icon": "🏆",
|
||||
"name_pl": "Bezbłędny", "name_ru": "Безошибочный", "name_uk": "Безпомилковий", "name_en": "Flawless",
|
||||
"desc_pl": "Odgadnij słowo bez żadnej pomyłki", "desc_ru": "Угадать слово без единой ошибки", "desc_uk": "Відгадати слово без жодної помилки", "desc_en": "Guess a word without any mistakes",
|
||||
"type": "quiz_no_mistakes", "value": 1},
|
||||
|
||||
# ===== Bubble Words (8) =====
|
||||
{"code": "bubble_5", "category": "bubblewords", "icon": "🫧",
|
||||
"name_pl": "Początkujący", "name_ru": "Начинающий", "name_uk": "Початківець", "name_en": "Beginner",
|
||||
"desc_pl": "Ukończ 5 poziomów w Bąbelkach słów", "desc_ru": "Пройти 5 уровней в Пузырях слов", "desc_uk": "Пройти 5 рівнів у Бульбашках слів", "desc_en": "Complete 5 levels in Bubble Words",
|
||||
"type": "bubble_levels", "value": 5},
|
||||
|
||||
{"code": "bubble_25", "category": "bubblewords", "icon": "🌟",
|
||||
"name_pl": "Bąbelkowy mistrz", "name_ru": "Пузырьковый мастер", "name_uk": "Бульбашковий майстер", "name_en": "Bubble Master",
|
||||
"desc_pl": "Ukończ 25 poziomów w Bąbelkach słów", "desc_ru": "Пройти 25 уровней в Пузырях слов", "desc_uk": "Пройти 25 рівнів у Бульбашках слів", "desc_en": "Complete 25 levels in Bubble Words",
|
||||
"type": "bubble_levels", "value": 25},
|
||||
|
||||
{"code": "bubble_50", "category": "bubblewords", "icon": "💪",
|
||||
"name_pl": "Bańkowy wojownik", "name_ru": "Пузырьковый воин", "name_uk": "Бульбашковий воїн", "name_en": "Bubble Warrior",
|
||||
"desc_pl": "Ukończ 50 poziomów w Bąbelkach słów", "desc_ru": "Пройти 50 уровней в Пузырях слов", "desc_uk": "Пройти 50 рівнів у Бульбашках слів", "desc_en": "Complete 50 levels in Bubble Words",
|
||||
"type": "bubble_levels", "value": 50},
|
||||
|
||||
{"code": "bubble_100", "category": "bubblewords", "icon": "👑",
|
||||
"name_pl": "Król bąbelków", "name_ru": "Король пузырей", "name_uk": "Король бульбашок", "name_en": "Bubble King",
|
||||
"desc_pl": "Ukończ 100 poziomów w Bąbelkach słów", "desc_ru": "Пройти 100 уровней в Пузырях слов", "desc_uk": "Пройти 100 рівнів у Бульбашках слів", "desc_en": "Complete 100 levels in Bubble Words",
|
||||
"type": "bubble_levels", "value": 100},
|
||||
|
||||
{"code": "bubble_no_hints", "category": "bubblewords", "icon": "💡",
|
||||
"name_pl": "Samodzielny", "name_ru": "Самостоятельный", "name_uk": "Самостійний", "name_en": "Independent",
|
||||
"desc_pl": "Ukończ poziom bez podpowiedzi", "desc_ru": "Пройти уровень без подсказок", "desc_uk": "Пройти рівень без підказок", "desc_en": "Complete a level without hints",
|
||||
"type": "bubble_no_hints", "value": 1},
|
||||
|
||||
{"code": "bubble_perfect", "category": "bubblewords", "icon": "✨",
|
||||
"name_pl": "Perfekcyjny", "name_ru": "Идеальный", "name_uk": "Ідеальний", "name_en": "Perfect",
|
||||
"desc_pl": "Ukończ poziom z maksymalną punktacją (100)", "desc_ru": "Пройти уровень с максимальным счётом (100)", "desc_uk": "Пройти рівень з максимальним рахунком (100)", "desc_en": "Complete a level with maximum score (100)",
|
||||
"type": "bubble_perfect", "value": 1},
|
||||
|
||||
{"code": "bubble_speed", "category": "bubblewords", "icon": "🚀",
|
||||
"name_pl": "Szybki bańki", "name_ru": "Быстрые пузыри", "name_uk": "Швидкі бульбашки", "name_en": "Fast Bubbles",
|
||||
"desc_pl": "Ukończ poziom w mniej niż 45 sekund", "desc_ru": "Пройти уровень быстрее 45 секунд", "desc_uk": "Пройти рівень швидше 45 секунд", "desc_en": "Complete a level in under 45 seconds",
|
||||
"type": "bubble_speed", "value": 45},
|
||||
|
||||
{"code": "bubble_speed_30", "category": "bubblewords", "icon": "⚡",
|
||||
"name_pl": "Bąbelkowa błyskawica", "name_ru": "Пузырьковая молния", "name_uk": "Бульбашкова блискавка", "name_en": "Bubble Lightning",
|
||||
"desc_pl": "Ukończ poziom w mniej niż 30 sekund", "desc_ru": "Пройти уровень быстрее 30 секунд", "desc_uk": "Пройти рівень швидше 30 секунд", "desc_en": "Complete a level in under 30 seconds",
|
||||
"type": "bubble_speed", "value": 30},
|
||||
|
||||
# ===== General (15) =====
|
||||
{"code": "score_1000", "category": "general", "icon": "🌟",
|
||||
"name_pl": "Wschodząca gwiazda", "name_ru": "Восходящая звезда", "name_uk": "Висхідна зірка", "name_en": "Rising Star",
|
||||
"desc_pl": "Zdobądź 1000 punktów", "desc_ru": "Набрать 1000 очков", "desc_uk": "Набрати 1000 балів", "desc_en": "Earn 1000 points",
|
||||
"type": "total_score", "value": 1000},
|
||||
|
||||
{"code": "score_5000", "category": "general", "icon": "💎",
|
||||
"name_pl": "Diament", "name_ru": "Бриллиант", "name_uk": "Діамант", "name_en": "Diamond",
|
||||
"desc_pl": "Zdobądź 5000 punktów", "desc_ru": "Набрать 5000 очков", "desc_uk": "Набрати 5000 балів", "desc_en": "Earn 5000 points",
|
||||
"type": "total_score", "value": 5000},
|
||||
|
||||
{"code": "score_10000", "category": "general", "icon": "💯",
|
||||
"name_pl": "Legenda", "name_ru": "Легенда", "name_uk": "Легенда", "name_en": "Legend",
|
||||
"desc_pl": "Zdobądź 10000 punktów", "desc_ru": "Набрать 10000 очков", "desc_uk": "Набрати 10000 балів", "desc_en": "Earn 10000 points",
|
||||
"type": "total_score", "value": 10000},
|
||||
|
||||
{"code": "score_25000", "category": "general", "icon": "🦄",
|
||||
"name_pl": "Jednorożec", "name_ru": "Единорог", "name_uk": "Єдиноріг", "name_en": "Unicorn",
|
||||
"desc_pl": "Zdobądź 25000 punktów", "desc_ru": "Набрать 25000 очков", "desc_uk": "Набрати 25000 балів", "desc_en": "Earn 25000 points",
|
||||
"type": "total_score", "value": 25000},
|
||||
|
||||
{"code": "rank_1", "category": "general", "icon": "🏆",
|
||||
"name_pl": "Mistrz", "name_ru": "Чемпион", "name_uk": "Чемпіон", "name_en": "Champion",
|
||||
"desc_pl": "Zajmij 1 miejsce w rankingu", "desc_ru": "Занять 1 место в рейтинге", "desc_uk": "Зайняти 1 місце в рейтингу", "desc_en": "Reach 1st place on leaderboard",
|
||||
"type": "rank_reached", "value": 1},
|
||||
|
||||
{"code": "rank_top3", "category": "general", "icon": "🥇",
|
||||
"name_pl": "Medalista", "name_ru": "Призёр", "name_uk": "Призер", "name_en": "Medalist",
|
||||
"desc_pl": "Zajmij miejsce w top 3", "desc_ru": "Попасть в топ-3 рейтинга", "desc_uk": "Потрапити в топ-3 рейтингу", "desc_en": "Reach top 3 on leaderboard",
|
||||
"type": "rank_reached", "value": 3},
|
||||
|
||||
{"code": "login_3", "category": "general", "icon": "📊",
|
||||
"name_pl": "Stały gracz", "name_ru": "Постоянный игрок", "name_uk": "Постійний гравець", "name_en": "Regular Player",
|
||||
"desc_pl": "Zaloguj się 3 dni z rzędu", "desc_ru": "Заходить 3 дня подряд", "desc_uk": "Заходити 3 дні поспіль", "desc_en": "Log in 3 days in a row",
|
||||
"type": "login_streak", "value": 3},
|
||||
|
||||
{"code": "login_7", "category": "general", "icon": "🔥",
|
||||
"name_pl": "Tygodniowa seria", "name_ru": "Недельная серия", "name_uk": "Тижнева серія", "name_en": "Weekly Streak",
|
||||
"desc_pl": "Zaloguj się 7 dni z rzędu", "desc_ru": "Заходить 7 дней подряд", "desc_uk": "Заходити 7 днів поспіль", "desc_en": "Log in 7 days in a row",
|
||||
"type": "login_streak", "value": 7},
|
||||
|
||||
{"code": "change_language", "category": "general", "icon": "🌍",
|
||||
"name_pl": "Poliglota", "name_ru": "Полиглот", "name_uk": "Поліглот", "name_en": "Polyglot",
|
||||
"desc_pl": "Zmień język interfejsu", "desc_ru": "Сменить язык интерфейса", "desc_uk": "Змінити мову інтерфейсу", "desc_en": "Change interface language",
|
||||
"type": "language_changed", "value": 1},
|
||||
|
||||
# 🔧 ИСПРАВЛЕНО: Заменено достижение "Открой Грамматику" на "Открой меню учителя"
|
||||
{"code": "open_teacher_menu", "category": "general", "icon": "🎓",
|
||||
"name_pl": "Uczeń", "name_ru": "Ученик", "name_uk": "Учень", "name_en": "Student",
|
||||
"desc_pl": "Otwórz menu nauczyciela", "desc_ru": "Открыть меню учителя", "desc_uk": "Відкрити меню вчителя", "desc_en": "Open the teacher menu",
|
||||
"type": "teacher_menu_opened", "value": 1},
|
||||
|
||||
{"code": "open_cheatsheets", "category": "general", "icon": "📝",
|
||||
"name_pl": "Ciekawski", "name_ru": "Любознательный", "name_uk": "Допитливий", "name_en": "Curious",
|
||||
"desc_pl": "Otwórz sekcję Ściągi", "desc_ru": "Открыть раздел Шпаргалки", "desc_uk": "Відкрити розділ Шпаргалки", "desc_en": "Open the Cheatsheets section",
|
||||
"type": "cheatsheets_opened", "value": 1},
|
||||
|
||||
{"code": "open_pronunciation", "category": "general", "icon": "🔊",
|
||||
"name_pl": "Słuchacz", "name_ru": "Слушатель", "name_uk": "Слухач", "name_en": "Listener",
|
||||
"desc_pl": "Otwórz sekcję Wymowa", "desc_ru": "Открыть раздел Произношение", "desc_uk": "Відкрити розділ Вимова", "desc_en": "Open the Pronunciation section",
|
||||
"type": "pronunciation_opened", "value": 1},
|
||||
|
||||
{"code": "open_ai_assistant", "category": "general", "icon": "🤖",
|
||||
"name_pl": "Asystent AI", "name_ru": "AI-помощник", "name_uk": "AI-помічник", "name_en": "AI Assistant",
|
||||
"desc_pl": "Otwórz Asystenta AI", "desc_ru": "Открыть AI-помощника", "desc_uk": "Відкрити AI-помічника", "desc_en": "Open the AI Assistant",
|
||||
"type": "ai_assistant_opened", "value": 1},
|
||||
|
||||
{"code": "open_alphabet", "category": "general", "icon": "🔤",
|
||||
"name_pl": "Alfabetysta", "name_ru": "Алфавитист", "name_uk": "Алфавітист", "name_en": "Alphabetist",
|
||||
"desc_pl": "Otwórz sekcję Alfabet", "desc_ru": "Открыть раздел Алфавит", "desc_uk": "Відкрити розділ Алфавіт", "desc_en": "Open the Alphabet section",
|
||||
"type": "alphabet_opened", "value": 1},
|
||||
|
||||
{"code": "open_rules", "category": "general", "icon": "📐",
|
||||
"name_pl": "Gramatyk", "name_ru": "Грамматик", "name_uk": "Граматик", "name_en": "Grammarian",
|
||||
"desc_pl": "Otwórz sekcję Zasady", "desc_ru": "Открыть раздел Правила", "desc_uk": "Відкрити розділ Правила", "desc_en": "Open the Rules section",
|
||||
"type": "rules_opened", "value": 1},
|
||||
|
||||
# 🔧 НОВОЕ: Достижение за открытие Прогресса
|
||||
{"code": "open_progress", "category": "general", "icon": "📊",
|
||||
"name_pl": "Planista", "name_ru": "Планировщик", "name_uk": "Планувальник", "name_en": "Planner",
|
||||
"desc_pl": "Otwórz sekcję Postęp", "desc_ru": "Открыть раздел Прогресс", "desc_uk": "Відкрити розділ Прогрес", "desc_en": "Open the Progress section",
|
||||
"type": "progress_opened", "value": 1},
|
||||
]
|
||||
|
||||
|
||||
async def init_achievements():
|
||||
"""Добавляет новые достижения, не трогая существующие"""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
for a in ACHIEVEMENTS_DATA:
|
||||
exists = await conn.fetchval(
|
||||
"SELECT 1 FROM achievements WHERE code = $1", a["code"]
|
||||
)
|
||||
if not exists:
|
||||
await conn.execute(
|
||||
"""INSERT INTO achievements (code, category, icon,
|
||||
name_pl, name_ru, name_uk, name_en,
|
||||
description_pl, description_ru, description_uk, description_en,
|
||||
requirement_type, requirement_value)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)""",
|
||||
a["code"], a["category"], a["icon"],
|
||||
a["name_pl"], a["name_ru"], a["name_uk"], a["name_en"],
|
||||
a["desc_pl"], a["desc_ru"], a["desc_uk"], a["desc_en"],
|
||||
a["type"], a["value"]
|
||||
)
|
||||
else:
|
||||
# 🔧 НОВОЕ: Обновляем существующие достижения (на случай изменения названий)
|
||||
await conn.execute("""
|
||||
UPDATE achievements SET
|
||||
category = $2,
|
||||
icon = $3,
|
||||
name_pl = $4,
|
||||
name_ru = $5,
|
||||
name_uk = $6,
|
||||
name_en = $7,
|
||||
description_pl = $8,
|
||||
description_ru = $9,
|
||||
description_uk = $10,
|
||||
description_en = $11,
|
||||
requirement_type = $12,
|
||||
requirement_value = $13
|
||||
WHERE code = $1
|
||||
""", a["code"], a["category"], a["icon"],
|
||||
a["name_pl"], a["name_ru"], a["name_uk"], a["name_en"],
|
||||
a["desc_pl"], a["desc_ru"], a["desc_uk"], a["desc_en"],
|
||||
a["type"], a["value"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_all_achievements(request: Request):
|
||||
"""Получить все достижения"""
|
||||
await require_auth(request)
|
||||
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetch("SELECT * FROM achievements ORDER BY id")
|
||||
|
||||
return {
|
||||
"achievements": [dict(row) for row in rows]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/user")
|
||||
async def get_user_achievements(request: Request):
|
||||
"""Получить достижения пользователя"""
|
||||
telegram_id, _ = await require_auth(request)
|
||||
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetch("""
|
||||
SELECT a.*,
|
||||
CASE WHEN ua.user_id IS NOT NULL THEN TRUE ELSE FALSE END as unlocked,
|
||||
ua.unlocked_at
|
||||
FROM achievements a
|
||||
LEFT JOIN user_achievements ua ON a.id = ua.achievement_id AND ua.user_id = $1
|
||||
ORDER BY a.id
|
||||
""", telegram_id)
|
||||
|
||||
achievements = []
|
||||
for row in rows:
|
||||
a = dict(row)
|
||||
if a.get("unlocked_at"):
|
||||
a["unlocked_at"] = a["unlocked_at"].isoformat()
|
||||
achievements.append(a)
|
||||
|
||||
total = len(achievements)
|
||||
unlocked = sum(1 for a in achievements if a["unlocked"])
|
||||
|
||||
return {
|
||||
"achievements": achievements,
|
||||
"total": total,
|
||||
"unlocked": unlocked,
|
||||
"progress_percent": round((unlocked / total * 100), 1) if total > 0 else 0
|
||||
}
|
||||
|
||||
|
||||
async def check_and_award_achievements(telegram_id: int, achievement_type: str, value: int = 1):
|
||||
"""
|
||||
Проверяет и выдаёт достижения пользователю.
|
||||
Возвращает названия на языке пользователя.
|
||||
"""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
user_lang = await conn.fetchval(
|
||||
"SELECT app_language FROM users WHERE telegram_id = $1", telegram_id
|
||||
)
|
||||
if not user_lang or user_lang not in ["pl", "ru", "uk", "en"]:
|
||||
user_lang = "pl"
|
||||
|
||||
matching = await conn.fetch("""
|
||||
SELECT a.* FROM achievements a
|
||||
WHERE a.requirement_type = $1
|
||||
AND a.requirement_value <= $2
|
||||
AND a.id NOT IN (
|
||||
SELECT achievement_id FROM user_achievements WHERE user_id = $3
|
||||
)
|
||||
""", achievement_type, value, telegram_id)
|
||||
|
||||
newly_unlocked = []
|
||||
name_field = f"name_{user_lang}"
|
||||
|
||||
for ach in matching:
|
||||
await conn.execute(
|
||||
"INSERT INTO user_achievements (user_id, achievement_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
||||
telegram_id, ach["id"]
|
||||
)
|
||||
newly_unlocked.append({
|
||||
"id": ach["id"],
|
||||
"code": ach["code"],
|
||||
"icon": ach["icon"],
|
||||
"name": ach[name_field],
|
||||
})
|
||||
|
||||
return newly_unlocked
|
||||
|
||||
|
||||
@router.post("/track-login")
|
||||
async def track_login(request: Request):
|
||||
"""Отмечает ежедневный вход пользователя и проверяет достижения серии"""
|
||||
telegram_id, _ = await require_auth(request)
|
||||
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"INSERT INTO daily_logins (user_id, login_date) VALUES ($1, CURRENT_DATE) ON CONFLICT DO NOTHING",
|
||||
telegram_id
|
||||
)
|
||||
|
||||
streak = await conn.fetchval("""
|
||||
WITH dates AS (
|
||||
SELECT login_date,
|
||||
login_date - ROW_NUMBER() OVER (ORDER BY login_date DESC)::int AS grp
|
||||
FROM daily_logins
|
||||
WHERE user_id = $1
|
||||
ORDER BY login_date DESC
|
||||
)
|
||||
SELECT COUNT(*) FROM dates WHERE grp = (SELECT grp FROM dates LIMIT 1)
|
||||
""", telegram_id)
|
||||
|
||||
streak = streak or 0
|
||||
|
||||
new_achievements = await check_and_award_achievements(telegram_id, "login_streak", streak)
|
||||
|
||||
return {
|
||||
"login_streak": streak,
|
||||
"new_achievements": new_achievements
|
||||
}
|
||||
|
||||
|
||||
# 🔧 НОВОЕ: Трекинг открытия меню учителя
|
||||
@router.post("/track-teacher-menu")
|
||||
async def track_teacher_menu_opened(request: Request):
|
||||
"""Отмечает открытие меню учителя и выдаёт достижение"""
|
||||
telegram_id, _ = await require_auth(request)
|
||||
|
||||
new_achievements = await check_and_award_achievements(telegram_id, "teacher_menu_opened", 1)
|
||||
|
||||
return {
|
||||
"new_achievements": new_achievements
|
||||
}
|
||||
|
||||
|
||||
# 🔧 НОВОЕ: Трекинг открытия Прогресса
|
||||
@router.post("/track-progress")
|
||||
async def track_progress_opened(request: Request):
|
||||
"""Отмечает открытие раздела Прогресс и выдаёт достижение"""
|
||||
telegram_id, _ = await require_auth(request)
|
||||
|
||||
new_achievements = await check_and_award_achievements(telegram_id, "progress_opened", 1)
|
||||
|
||||
return {
|
||||
"new_achievements": new_achievements
|
||||
}
|
||||
|
||||
|
||||
@router.post("/track-cheatsheets")
|
||||
async def track_cheatsheets_opened(request: Request):
|
||||
"""Отмечает открытие раздела Шпаргалки и выдаёт достижение"""
|
||||
telegram_id, _ = await require_auth(request)
|
||||
|
||||
new_achievements = await check_and_award_achievements(telegram_id, "cheatsheets_opened", 1)
|
||||
|
||||
return {
|
||||
"new_achievements": new_achievements
|
||||
}
|
||||
|
||||
|
||||
@router.post("/track-pronunciation")
|
||||
async def track_pronunciation_opened(request: Request):
|
||||
"""Отмечает открытие раздела Произношение и выдаёт достижение"""
|
||||
telegram_id, _ = await require_auth(request)
|
||||
|
||||
new_achievements = await check_and_award_achievements(telegram_id, "pronunciation_opened", 1)
|
||||
|
||||
return {
|
||||
"new_achievements": new_achievements
|
||||
}
|
||||
|
||||
|
||||
@router.post("/track-ai-assistant")
|
||||
async def track_ai_assistant_opened(request: Request):
|
||||
"""Отмечает открытие AI-помощника и выдаёт достижение"""
|
||||
telegram_id, _ = await require_auth(request)
|
||||
|
||||
new_achievements = await check_and_award_achievements(telegram_id, "ai_assistant_opened", 1)
|
||||
|
||||
return {
|
||||
"new_achievements": new_achievements
|
||||
}
|
||||
|
||||
|
||||
@router.post("/track-alphabet")
|
||||
async def track_alphabet_opened(request: Request):
|
||||
"""Отмечает открытие раздела Алфавит и выдаёт достижение"""
|
||||
telegram_id, _ = await require_auth(request)
|
||||
|
||||
new_achievements = await check_and_award_achievements(telegram_id, "alphabet_opened", 1)
|
||||
|
||||
return {
|
||||
"new_achievements": new_achievements
|
||||
}
|
||||
|
||||
|
||||
@router.post("/track-rules")
|
||||
async def track_rules_opened(request: Request):
|
||||
"""Отмечает открытие раздела Правила и выдаёт достижение"""
|
||||
telegram_id, _ = await require_auth(request)
|
||||
|
||||
new_achievements = await check_and_award_achievements(telegram_id, "rules_opened", 1)
|
||||
|
||||
return {
|
||||
"new_achievements": new_achievements
|
||||
}
|
||||
Reference in New Issue
Block a user