diff --git a/TASKS.md b/TASKS.md index b540c78..68bc22b 100644 --- a/TASKS.md +++ b/TASKS.md @@ -218,3 +218,26 @@ - [x] Сделать задачи в виджете «Недельный план» на главном экране кликабельными. - [x] Улучшить обратную связь при выполнении задач (Haptics, анимации). - [x] Подготовить релиз v1.13.0. + +# Список задач по улучшению приложения (Цикл 14) - В РАБОТЕ 🏗️ + +## 🎮 Геймификация и Прогресс +- [ ] Внедрить интерактивную «Карту Квеста» (Quest Map) на первые 30 дней трезвости. +- [ ] Добавить визуализацию этапов квеста с уникальными иконками для ключевых вех (1, 3, 7, 14, 30 дней). + +## 🤖 AI-Ассистент и Персонализация +- [ ] Реализовать функцию «Утренний брифинг» (Morning Briefing) при первом запуске приложения. +- [ ] Добавить поддержку голосового ввода (Voice Input) для дневника (если возможно технически) или аудио-заметок. + +## 📚 Контент и Обучение +- [ ] Внедрить систему «Мини-квизов» в конце статей для проверки знаний. +- [ ] Добавить 10 новых «быстрых советов» (Quick Tips) для SOS-ситуаций. + +## 💬 Сообщество и Социализация +- [ ] Реализовать «Групповые челенджи» в ленте сообщества. +- [ ] Улучшить систему уведомлений о реакциях на посты. + +## 🎨 Интерфейс и UX +- [ ] Обновить дизайн карточек на главной странице для более современного вида. +- [ ] Оптимизировать производительность списков в Сообществе и Базе знаний. +- [ ] Подготовить релиз v1.14.0 и обновить скриншоты. diff --git a/app/(tabs)/community.tsx b/app/(tabs)/community.tsx index cd72463..61c8e03 100644 --- a/app/(tabs)/community.tsx +++ b/app/(tabs)/community.tsx @@ -7,7 +7,7 @@ import { MaterialIcons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { LinearGradient } from 'expo-linear-gradient'; -import { CommunityService, SuccessStory, SupportPost, ExpertQA, ReactionType, CommunityGoal } from '../../services/communityService'; +import { CommunityService, SuccessStory, SupportPost, ExpertQA, ReactionType, CommunityGoal, GroupChallenge } from '../../services/communityService'; import Animated, { FadeInUp, FadeInRight, useSharedValue, useAnimatedStyle, withSpring, withSequence, withTiming } from 'react-native-reanimated'; import { Skeleton } from '../../components/Skeleton'; @@ -195,6 +195,7 @@ export default function CommunityPage() { const [posts, setPosts] = useState([]); const [expertQA, setExpertQA] = useState([]); const [communityGoals, setCommunityGoals] = useState([]); + const [groupChallenges, setGroupChallenges] = useState([]); const [circles, setCircles] = useState([]); const [selectedCircle, setSelectedCircle] = useState('all'); const [isModalVisible, setIsModalVisible] = useState(false); @@ -215,6 +216,7 @@ export default function CommunityPage() { setStories(CommunityService.getSuccessStories()); setExpertQA(CommunityService.getExpertQA()); setCommunityGoals(CommunityService.getCommunityGoals()); + setGroupChallenges(CommunityService.getGroupChallenges()); const loadedPosts = await CommunityService.getSupportPosts(); const dailyThread = CommunityService.getDailyThread(); @@ -314,6 +316,37 @@ export default function CommunityPage() { return ( + + Групповые челленджи + + + + {isLoading ? ( + [1, 2].map(i => ) + ) : ( + groupChallenges.map(challenge => ( + + + + {challenge.category} + + осталось {challenge.daysRemaining} дн. + + {challenge.title} + {challenge.description} + + + {challenge.participants} участников + + + )) + )} + + Цели сообщества @@ -784,6 +817,61 @@ const styles = StyleSheet.create({ paddingHorizontal: 15, gap: 15 }, + challengeCard: { + width: 280, + backgroundColor: '#fff', + borderRadius: 16, + padding: 15, + marginRight: 15, + elevation: 3, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + challengeHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 10, + }, + challengeBadge: { + backgroundColor: '#E8F5E8', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 8, + }, + challengeBadgeText: { + color: '#2E7D4A', + fontSize: 10, + fontWeight: 'bold', + }, + challengeDays: { + fontSize: 11, + color: '#F44336', + fontWeight: '500', + }, + challengeTitle: { + fontSize: 16, + fontWeight: 'bold', + color: '#333', + marginBottom: 5, + }, + challengeDesc: { + fontSize: 12, + color: '#666', + marginBottom: 10, + lineHeight: 18, + }, + challengeFooter: { + flexDirection: 'row', + alignItems: 'center', + gap: 5, + }, + challengeParticipants: { + fontSize: 11, + color: '#666', + }, storyCard: { backgroundColor: 'white', width: screenWidth * 0.7, diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index d6625f0..b3d1e34 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -24,7 +24,10 @@ import { MemoizedHealthMetric } from '../../components/home/HealthMetric'; import { StatCard } from '../../components/home/StatCard'; import { DailyMotivationService, MotivationQuote, RecoveryTip } from '../../services/dailyMotivationService'; import { LineChart } from 'react-native-chart-kit'; -import { AICoachService, WeeklyRoadmap } from '../../services/AICoachService'; +import { AICoachService, WeeklyRoadmap, MorningBriefing } from '../../services/AICoachService'; +import questService, { QuestMilestone } from '../../services/questService'; +import { QuestMap } from '../../components/QuestMap'; +import AsyncStorage from '@react-native-async-storage/async-storage'; const AchievementSystem = React.lazy(() => import('../../components/AchievementSystem')); const CrisisIntervention = React.lazy(() => import('../../components/CrisisIntervention')); @@ -133,6 +136,9 @@ function HomePage() { const [dailyTip, setDailyTip] = useState(null); const [weeklyRoadmap, setWeeklyRoadmap] = useState(null); const [isUpdatingRoadmap, setIsUpdatingRoadmap] = useState(false); + const [questMilestones, setQuestMilestones] = useState([]); + const [showBriefing, setShowBriefing] = useState(false); + const [morningBriefing, setMorningBriefing] = useState(null); const [alertConfig, setAlertConfig] = useState<{ visible: boolean; title: string; @@ -178,8 +184,28 @@ function HomePage() { setWeeklyRoadmap(roadmap); } }; + + const loadQuest = async () => { + const milestones = await questService.updateQuestProgress(soberDays); + setQuestMilestones(milestones); + }; + + const checkBriefing = async () => { + if (!userProfile?.id) return; + const lastBriefing = await AsyncStorage.getItem(`last_briefing_${userProfile.id}`); + const today = new Date().toDateString(); + if (lastBriefing !== today) { + const briefing = await AICoachService.getMorningBriefing(userProfile.id, soberDays, mood); + setMorningBriefing(briefing); + setShowBriefing(true); + await AsyncStorage.setItem(`last_briefing_${userProfile.id}`, today); + } + }; + loadRoadmap(); - }, [pulseValue, userProfile?.id, soberDays]); + loadQuest(); + checkBriefing(); + }, [pulseValue, userProfile?.id, soberDays, mood]); const showWebAlert = useCallback((title: string, message: string, onOk?: () => void) => { if (Platform.OS === 'web') { @@ -357,6 +383,8 @@ function HomePage() { + + {weeklyRoadmap && ( @@ -703,6 +731,54 @@ function HomePage() { + {/* Morning Briefing Modal */} + setShowBriefing(false)} + > + + + + + {morningBriefing?.title} + + + + Установка дня: {morningBriefing?.focus} + + Ваш план: + {morningBriefing?.plan.map((item, idx) => ( + + + {item} + + ))} + + Советы: + {morningBriefing?.quickTips.map((tip, idx) => ( + + + {tip} + + ))} + + + "{morningBriefing?.motivation}" + + + + setShowBriefing(false)} + > + Понятно + + + + + {Platform.OS === 'web' && ( diff --git a/components/QuestMap.tsx b/components/QuestMap.tsx new file mode 100644 index 0000000..950ddb0 --- /dev/null +++ b/components/QuestMap.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useAppTheme } from '../context/ThemeContext'; +import { QuestMilestone } from '../services/questService'; + +interface QuestMapProps { + milestones: QuestMilestone[]; + currentSoberDays: number; +} + +export const QuestMap: React.FC = ({ milestones, currentSoberDays }) => { + const { colors } = useAppTheme(); + + return ( + + Квест: Первые 30 дней + + {milestones.map((milestone, index) => { + const isCompleted = currentSoberDays >= milestone.day; + const isCurrent = !isCompleted && (index === 0 || currentSoberDays >= milestones[index - 1].day); + + return ( + + + + + {milestone.day} день + + {milestone.title} + + + {index < milestones.length - 1 && ( + + )} + + ); + })} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginVertical: 16, + paddingHorizontal: 16, + }, + title: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 12, + }, + scrollContent: { + paddingRight: 32, + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 10, + }, + milestoneWrapper: { + alignItems: 'center', + width: 100, + position: 'relative', + }, + node: { + width: 50, + height: 50, + borderRadius: 25, + justifyContent: 'center', + alignItems: 'center', + zIndex: 2, + elevation: 3, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 2, + }, + dayText: { + marginTop: 8, + fontSize: 12, + fontWeight: '600', + }, + nodeTitle: { + fontSize: 10, + marginTop: 2, + textAlign: 'center', + }, + connector: { + position: 'absolute', + top: 25, + left: 75, + width: 50, + height: 3, + zIndex: 1, + } +}); diff --git a/services/AICoachService.ts b/services/AICoachService.ts index 3114b63..f635ee4 100644 --- a/services/AICoachService.ts +++ b/services/AICoachService.ts @@ -119,6 +119,14 @@ export interface AICoachChallenge { icon: string; } +export interface MorningBriefing { + title: string; + plan: string[]; + motivation: string; + focus: string; + quickTips: string[]; +} + const MEMORY_STORAGE_KEY = 'sober_path_ai_memory'; const CHALLENGES_STORAGE_KEY = 'sober_path_ai_challenges'; const ROADMAP_STORAGE_KEY = 'sober_path_ai_roadmap'; @@ -615,6 +623,53 @@ export class AICoachService { return success(updated); } + static async getMorningBriefing(userId: string, soberDays: number, mood: number): Promise { + const allQuickTips = [ + "Пейте больше воды при появлении тяги", + "Сделайте 10 глубоких вдохов при стрессе", + "Позвоните другу, если станет трудно", + "Съешьте что-нибудь сладкое (глюкоза помогает мозгу)", + "Примите контрастный душ для перезагрузки", + "Запишите 3 вещи, за которые вы благодарны сегодня", + "Смените обстановку: просто выйдете в другую комнату или на улицу", + "Послушайте любимую энергичную музыку", + "Сделайте 15 приседаний, чтобы переключить внимание", + "Напомните себе: тяга длится всего 15 минут", + "Посмотрите на фото близких или целей, ради которых вы это делаете", + "Прочитайте одну главу книги или статью в нашем приложении" + ]; + + // Выбираем 3 случайных совета + const quickTips = [...allQuickTips] + .sort(() => 0.5 - Math.random()) + .slice(0, 3); + + let title = "Ваш план на сегодня"; + let plan = [ + "Отметить день в календаре", + "Прочитать одну статью о восстановлении", + "Сделать 5-минутную медитацию" + ]; + let motivation = "Каждый день без алкоголя делает вас сильнее и свободнее."; + let focus = "Сохранение спокойствия и осознанности"; + + if (mood <= 2) { + title = "Поддержка в трудный день"; + plan = [ + "Избегать триггерных мест и людей", + "Слушать успокаивающее SOS-аудио", + "Записать свои чувства в дневник" + ]; + focus = "Эмоциональная безопасность"; + } + + if (soberDays > 30) { + plan.push("Поделиться опытом в сообществе"); + } + + return { title, plan, motivation, focus, quickTips }; + } + private static extractTopics(message: string): string[] { const topics: Set = new Set(); const lowerMessage = message.toLowerCase(); diff --git a/services/communityService.ts b/services/communityService.ts index 2d9f71d..117d24e 100644 --- a/services/communityService.ts +++ b/services/communityService.ts @@ -42,6 +42,15 @@ export interface CommunityGoal { color: string; } +export interface GroupChallenge { + id: string; + title: string; + description: string; + participants: number; + daysRemaining: number; + category: string; +} + const POSTS_STORAGE_KEY = 'sober_path_community_posts'; export class CommunityService { @@ -155,6 +164,27 @@ export class CommunityService { ]; } + static getGroupChallenges(): GroupChallenge[] { + return [ + { + id: 'c1', + title: 'Марафон "Чистый Октябрь"', + description: 'Продержимся весь месяц вместе! Еженедельные созвоны и поддержка.', + participants: 450, + daysRemaining: 12, + category: 'Марафон' + }, + { + id: 'c2', + title: 'Утренняя осознанность', + description: 'Группа для тех, кто практикует медитацию каждое утро.', + participants: 120, + daysRemaining: 5, + category: 'Привычки' + } + ]; + } + static async getSupportPosts(): Promise { const localPosts = await this.loadUserPosts(); const staticPosts: SupportPost[] = [ diff --git a/services/questService.ts b/services/questService.ts new file mode 100644 index 0000000..1fc42d6 --- /dev/null +++ b/services/questService.ts @@ -0,0 +1,54 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +export interface QuestMilestone { + id: string; + day: number; + title: string; + description: string; + reward: string; + isCompleted: boolean; +} + +const QUEST_STORAGE_KEY = 'sober_path_quest_progress'; + +export const QUEST_MILESTONES: QuestMilestone[] = [ + { id: '1', day: 1, title: 'Первый шаг', description: 'Первый день новой жизни', reward: 'Медаль новичка', isCompleted: false }, + { id: '3', day: 3, title: 'Установка', description: 'Три дня чистоты и ясности', reward: 'Бонус концентрации', isCompleted: false }, + { id: '7', day: 7, title: 'Неделя!', description: 'Семь дней — это серьезная победа', reward: 'Доступ к секретному уроку', isCompleted: false }, + { id: '14', day: 14, title: 'Две недели', description: 'Привычка начинает формироваться', reward: 'Эксклюзивная тема оформления', isCompleted: false }, + { id: '21', day: 21, title: 'Три недели', description: 'Организм восстанавливается', reward: 'Значок мастера дисциплины', isCompleted: false }, + { id: '30', day: 30, title: 'Месяц Свободы', description: '30 дней — фундамент заложен!', reward: 'Кубок Победителя', isCompleted: false }, +]; + +class QuestService { + async getQuestProgress(): Promise { + try { + const saved = await AsyncStorage.getItem(QUEST_STORAGE_KEY); + if (saved) { + return JSON.parse(saved); + } + return QUEST_MILESTONES; + } catch (e) { + console.error('Failed to load quest progress', e); + return QUEST_MILESTONES; + } + } + + async updateQuestProgress(soberDays: number): Promise { + const milestones = await this.getQuestProgress(); + const updated = milestones.map(m => ({ + ...m, + isCompleted: soberDays >= m.day + })); + await AsyncStorage.setItem(QUEST_STORAGE_KEY, JSON.stringify(updated)); + return updated; + } + + async getCurrentQuestLevel(soberDays: number): Promise { + const milestones = await this.getQuestProgress(); + const lastCompleted = [...milestones].reverse().find(m => soberDays >= m.day); + return lastCompleted ? milestones.indexOf(lastCompleted) + 1 : 0; + } +} + +export default new QuestService();