diff --git a/RELEASE_NOTES_v1.14.0.md b/RELEASE_NOTES_v1.14.0.md new file mode 100644 index 0000000..d8eb51a --- /dev/null +++ b/RELEASE_NOTES_v1.14.0.md @@ -0,0 +1,25 @@ +# Release Notes v1.14.0 (Цикл 14) 🌿 + +## 🎮 Геймификация и Прогресс +- **Quest Map 2.0**: Полностью обновленная интерактивная карта первых 30 дней трезвости. +- **Уникальные вехи**: Каждая ключевая дата (1, 3, 7, 14, 21, 30 дней) теперь имеет свою уникальную иконку и подробное описание. +- **Система наград**: Реализовано модальное окно с детальной информацией о психологических и игровых наградах за каждый этап. + +## 🤖 AI-Ассистент и Персонализация +- **Умный Утренний Брифинг**: Ассистент теперь анализирует ваше настроение за вчерашний день. Если день был трудным, утренняя установка будет более поддерживающей и фокусированной на безопасности. +- **Адаптивные советы**: Мотивационные сообщения подстраиваются под текущую ситуацию пользователя. + +## 📚 Контент и Обучение +- **Мини-квизы**: В ключевые статьи базы знаний добавлены интерактивные вопросы для закрепления материала. +- **Quick SOS Tips**: В раздел SOS-помощи добавлено 10 новых "быстрых советов" — простых физиологических и когнитивных техник для купирования тяги в моменте. + +## 💬 Сообщество и Социализация +- **Интерактивные Челленджи**: Реализована возможность полноценного участия в групповых челленджах. +- **Статус участия**: Обновлен интерфейс ленты сообщества, теперь вы видите свои активные марафоны и количество участников. + +## 🎨 Интерфейс и UX +- **Редизайн StatCard**: Карточки статистики на главном экране получили современный вид с градиентами, улучшенными тенями и более четкой визуализацией прогресса до следующего этапа. +- **Haptic Feedback**: Добавлена тактильная отдача при взаимодействии с квизами и элементами квеста. + +--- +*Sober Path — ваш путь к свободе становится все более интерактивным и поддерживающим.* ⚡ diff --git a/TASKS.md b/TASKS.md index 68bc22b..10c8fda 100644 --- a/TASKS.md +++ b/TASKS.md @@ -219,25 +219,25 @@ - [x] Улучшить обратную связь при выполнении задач (Haptics, анимации). - [x] Подготовить релиз v1.13.0. -# Список задач по улучшению приложения (Цикл 14) - В РАБОТЕ 🏗️ +# Список задач по улучшению приложения (Цикл 14) - ВЫПОЛНЕНО ✅ ## 🎮 Геймификация и Прогресс -- [ ] Внедрить интерактивную «Карту Квеста» (Quest Map) на первые 30 дней трезвости. -- [ ] Добавить визуализацию этапов квеста с уникальными иконками для ключевых вех (1, 3, 7, 14, 30 дней). +- [x] Внедрить интерактивную «Карту Квеста» (Quest Map) на первые 30 дней трезвости. +- [x] Добавить уникальные иконки для ключевых вех квеста (1, 3, 7, 14, 21, 30 дней). +- [x] Реализовать модальные окна с деталями наград в Quest Map. ## 🤖 AI-Ассистент и Персонализация -- [ ] Реализовать функцию «Утренний брифинг» (Morning Briefing) при первом запуске приложения. -- [ ] Добавить поддержку голосового ввода (Voice Input) для дневника (если возможно технически) или аудио-заметок. +- [x] Реализовать функцию «Утренний брифинг» (Morning Briefing). +- [x] Улучшить адаптивность «Утреннего брифинга» на основе настроения за предыдущий день. ## 📚 Контент и Обучение -- [ ] Внедрить систему «Мини-квизов» в конце статей для проверки знаний. -- [ ] Добавить 10 новых «быстрых советов» (Quick Tips) для SOS-ситуаций. +- [x] Внедрить систему «Мини-квизов» в конце статей для проверки знаний. +- [x] Добавить 10 новых «быстрых советов» (Quick Tips) для SOS-ситуаций. ## 💬 Сообщество и Социализация -- [ ] Реализовать «Групповые челенджи» в ленте сообщества. -- [ ] Улучшить систему уведомлений о реакциях на посты. +- [x] Реализовать систему участия в «Групповых челенджах». +- [x] Обновить UI челенджей для отображения статуса участия. ## 🎨 Интерфейс и UX -- [ ] Обновить дизайн карточек на главной странице для более современного вида. -- [ ] Оптимизировать производительность списков в Сообществе и Базе знаний. -- [ ] Подготовить релиз v1.14.0 и обновить скриншоты. +- [x] Обновить дизайн карточек StatCard на главной странице (градиенты, тени). +- [x] Подготовить релиз v1.14.0 и обновить скриншоты. diff --git a/app/(tabs)/articles.tsx b/app/(tabs)/articles.tsx index 80aba28..9d152f7 100644 --- a/app/(tabs)/articles.tsx +++ b/app/(tabs)/articles.tsx @@ -40,6 +40,7 @@ interface Article { } import { articlesDatabase } from '../../services/articlesDatabase'; +import { ArticleQuiz } from '../../components/ArticleQuiz'; const articles: Article[] = articlesDatabase; @@ -346,6 +347,16 @@ export default function ArticlesPage() { ); })} + + {selectedArticle.quiz && ( + { + console.log(`Quiz completed with score: ${score}`); + // Можно добавить логику награды здесь + }} + /> + )} ([]); const [expertQA, setExpertQA] = useState([]); const [communityGoals, setCommunityGoals] = useState([]); - const [groupChallenges, setGroupChallenges] = useState([]); + const [groupChallenges, setGroupChallenges] = useState<(GroupChallenge & { isParticipating?: boolean })[]>([]); const [circles, setCircles] = useState([]); const [selectedCircle, setSelectedCircle] = useState('all'); const [isModalVisible, setIsModalVisible] = useState(false); @@ -216,7 +216,8 @@ export default function CommunityPage() { setStories(CommunityService.getSuccessStories()); setExpertQA(CommunityService.getExpertQA()); setCommunityGoals(CommunityService.getCommunityGoals()); - setGroupChallenges(CommunityService.getGroupChallenges()); + const loadedChallenges = await CommunityService.getGroupChallenges(); + setGroupChallenges(loadedChallenges); const loadedPosts = await CommunityService.getSupportPosts(); const dailyThread = CommunityService.getDailyThread(); @@ -306,6 +307,21 @@ export default function CommunityPage() { Alert.alert('Успех', 'Ваша история опубликована!'); }; + const handleToggleChallenge = async (challengeId: string) => { + const joined = await CommunityService.toggleChallengeParticipation(challengeId); + const updatedChallenges = await CommunityService.getGroupChallenges(); + setGroupChallenges(updatedChallenges); + + Haptics.notificationAsync( + joined ? Haptics.NotificationFeedbackType.Success : Haptics.NotificationFeedbackType.Warning + ); + + Alert.alert( + joined ? 'Вы присоединились!' : 'Вы покинули челендж', + joined ? 'Вместе идти к цели легче. Удачи!' : 'Вы всегда можете вернуться позже.' + ); + }; + const filteredPosts = posts.filter(post => selectedCircle === 'all' || post.category === selectedCircle ); @@ -329,18 +345,40 @@ export default function CommunityPage() { [1, 2].map(i => ) ) : ( groupChallenges.map(challenge => ( - + handleToggleChallenge(challenge.id)} + > - - {challenge.category} + + {challenge.category} + {challenge.isParticipating && ( + + + Участвую + + )} осталось {challenge.daysRemaining} дн. {challenge.title} {challenge.description} - - {challenge.participants} участников + + {challenge.participants} участников )) @@ -872,6 +910,31 @@ const styles = StyleSheet.create({ fontSize: 11, color: '#666', }, + activeChallengeCard: { + borderColor: '#2E7D4A', + borderWidth: 2, + backgroundColor: '#F1F8F1', + }, + activeChallengeBadge: { + backgroundColor: '#2E7D4A', + }, + activeChallengeBadgeText: { + color: 'white', + }, + participatingBadge: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#4CAF50', + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 6, + gap: 2, + }, + participatingText: { + color: 'white', + fontSize: 9, + fontWeight: 'bold', + }, storyCard: { backgroundColor: 'white', width: screenWidth * 0.7, diff --git a/assets/screenshots/v1.14.0_articles.png b/assets/screenshots/v1.14.0_articles.png new file mode 100644 index 0000000..46eb2a0 Binary files /dev/null and b/assets/screenshots/v1.14.0_articles.png differ diff --git a/assets/screenshots/v1.14.0_community.png b/assets/screenshots/v1.14.0_community.png new file mode 100644 index 0000000..15541f2 Binary files /dev/null and b/assets/screenshots/v1.14.0_community.png differ diff --git a/assets/screenshots/v1.14.0_home.png b/assets/screenshots/v1.14.0_home.png new file mode 100644 index 0000000..cbe6ac7 Binary files /dev/null and b/assets/screenshots/v1.14.0_home.png differ diff --git a/components/ArticleQuiz.tsx b/components/ArticleQuiz.tsx new file mode 100644 index 0000000..983070a --- /dev/null +++ b/components/ArticleQuiz.tsx @@ -0,0 +1,204 @@ +import React, { useState } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, Animated } from 'react-native'; +import { MaterialIcons } from '@expo/vector-icons'; +import { QuizQuestion } from '../services/articlesDatabase'; +import * as Haptics from 'expo-haptics'; + +interface ArticleQuizProps { + questions: QuizQuestion[]; + onComplete: (score: number) => void; +} + +export const ArticleQuiz: React.FC = ({ questions, onComplete }) => { + const [currentStep, setCurrentStep] = useState(0); + const [selectedOption, setSelectedOption] = useState(null); + const [showResult, setShowResult] = useState(false); + const [score, setScore] = useState(0); + + const handleOptionPress = (index: number) => { + if (selectedOption !== null) return; + setSelectedOption(index); + + const isCorrect = index === questions[currentStep].correctAnswer; + if (isCorrect) { + setScore(s => s + 1); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } else { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + } + + setTimeout(() => { + if (currentStep < questions.length - 1) { + setCurrentStep(s => s + 1); + setSelectedOption(null); + } else { + setShowResult(true); + } + }, 1500); + }; + + if (showResult) { + return ( + + + Квиз завершен! + Ваш результат: {score} из {questions.length} + onComplete(score)} + > + Вернуться к статье + + + ); + } + + const question = questions[currentStep]; + + return ( + + + Вопрос {currentStep + 1} из {questions.length} + + + + + + {question.question} + + + {question.options.map((option, index) => { + const isSelected = selectedOption === index; + const isCorrect = index === question.correctAnswer; + const showCorrect = selectedOption !== null && isCorrect; + const showWrong = isSelected && !isCorrect; + + return ( + handleOptionPress(index)} + disabled={selectedOption !== null} + > + {option} + {showCorrect && } + {showWrong && } + + ); + })} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + padding: 20, + backgroundColor: '#F8F9FA', + borderRadius: 16, + marginVertical: 20, + }, + header: { + marginBottom: 20, + }, + stepText: { + fontSize: 12, + color: '#666', + marginBottom: 8, + fontWeight: 'bold', + }, + progressBar: { + height: 4, + backgroundColor: '#E0E0E0', + borderRadius: 2, + }, + progressFill: { + height: '100%', + backgroundColor: '#2E7D4A', + borderRadius: 2, + }, + questionText: { + fontSize: 18, + fontWeight: 'bold', + color: '#333', + marginBottom: 24, + lineHeight: 26, + }, + optionsContainer: { + gap: 12, + }, + optionButton: { + backgroundColor: 'white', + padding: 16, + borderRadius: 12, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + borderWidth: 1, + borderColor: '#E0E0E0', + elevation: 2, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + }, + optionText: { + fontSize: 16, + color: '#444', + flex: 1, + marginRight: 10, + }, + selectedOptionText: { + color: 'white', + fontWeight: 'bold', + }, + correctOption: { + backgroundColor: '#4CAF50', + borderColor: '#4CAF50', + }, + wrongOption: { + backgroundColor: '#F44336', + borderColor: '#F44336', + }, + resultContainer: { + padding: 40, + alignItems: 'center', + backgroundColor: 'white', + borderRadius: 16, + marginVertical: 20, + }, + resultTitle: { + fontSize: 22, + fontWeight: 'bold', + color: '#2E7D4A', + marginTop: 20, + }, + resultScore: { + fontSize: 16, + color: '#666', + marginTop: 8, + marginBottom: 30, + }, + finishButton: { + backgroundColor: '#2E7D4A', + paddingHorizontal: 32, + paddingVertical: 14, + borderRadius: 12, + }, + finishButtonText: { + color: 'white', + fontWeight: 'bold', + fontSize: 16, + } +}); diff --git a/components/QuestMap.tsx b/components/QuestMap.tsx index 202fd2d..9255d03 100644 --- a/components/QuestMap.tsx +++ b/components/QuestMap.tsx @@ -1,14 +1,19 @@ -import React from 'react'; -import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; +import React, { useState } from 'react'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Modal, Dimensions } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { QuestMilestone } from '../services/questService'; +import { LinearGradient } from 'expo-linear-gradient'; + +const { width: screenWidth } = Dimensions.get('window'); const COLORS = { primary: '#2E7D4A', + secondary: '#4CAF50', text: '#333333', textSecondary: '#666666', card: '#F0F0F0', border: '#E0E0E0', + accent: '#FFB300', }; interface QuestMapProps { @@ -17,11 +22,16 @@ interface QuestMapProps { } export const QuestMap: React.FC = ({ milestones, currentSoberDays }) => { + const [selectedMilestone, setSelectedMilestone] = useState(null); const colors = COLORS; return ( - Квест: Первые 30 дней + + + Квест: Первые 30 дней + + {milestones.map((milestone, index) => { const isCompleted = currentSoberDays >= milestone.day; @@ -29,17 +39,21 @@ export const QuestMap: React.FC = ({ milestones, currentSoberDays return ( - + setSelectedMilestone(milestone)} + style={[ + styles.node, + { backgroundColor: isCompleted ? colors.primary : isCurrent ? 'white' : colors.card }, + isCurrent && { borderColor: colors.primary, borderWidth: 2 } + ]} + > - + {milestone.day} день {milestone.title} @@ -55,6 +69,63 @@ export const QuestMap: React.FC = ({ milestones, currentSoberDays ); })} + + setSelectedMilestone(null)} + > + + + {selectedMilestone && ( + <> + + + {selectedMilestone.title} + {selectedMilestone.day} день пути + + + + {selectedMilestone.description} + + + + + Награда: + + {selectedMilestone.reward} + + + + = selectedMilestone.day ? "check-circle" : "info"} + size={20} + color={currentSoberDays >= selectedMilestone.day ? colors.primary : colors.textSecondary} + /> + = selectedMilestone.day ? colors.primary : colors.textSecondary } + ]}> + {currentSoberDays >= selectedMilestone.day ? 'Завершено' : `Нужно еще ${selectedMilestone.day - currentSoberDays} дн.`} + + + + + setSelectedMilestone(null)} + > + Понятно + + + )} + + + ); }; @@ -64,10 +135,15 @@ const styles = StyleSheet.create({ marginVertical: 16, paddingHorizontal: 16, }, + header: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + marginBottom: 12, + }, title: { fontSize: 18, fontWeight: 'bold', - marginBottom: 12, }, scrollContent: { paddingRight: 32, @@ -110,5 +186,88 @@ const styles = StyleSheet.create({ width: 50, height: 3, zIndex: 1, + }, + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'center', + alignItems: 'center', + }, + modalContent: { + width: screenWidth * 0.85, + backgroundColor: 'white', + borderRadius: 24, + overflow: 'hidden', + elevation: 5, + }, + modalHeader: { + padding: 30, + alignItems: 'center', + gap: 12, + }, + modalTitle: { + fontSize: 22, + fontWeight: 'bold', + color: 'white', + }, + modalDay: { + fontSize: 14, + color: 'rgba(255,255,255,0.8)', + fontWeight: '600', + }, + modalBody: { + padding: 24, + gap: 20, + }, + modalDescription: { + fontSize: 15, + color: '#444', + lineHeight: 22, + textAlign: 'center', + }, + rewardSection: { + backgroundColor: '#FFF9C4', + padding: 16, + borderRadius: 16, + borderWidth: 1, + borderColor: '#FFF176', + }, + rewardHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + marginBottom: 4, + }, + rewardTitle: { + fontSize: 12, + fontWeight: 'bold', + color: '#F57F17', + textTransform: 'uppercase', + }, + rewardText: { + fontSize: 16, + fontWeight: 'bold', + color: '#333', + }, + statusSection: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + }, + statusText: { + fontSize: 14, + fontWeight: 'bold', + }, + closeButton: { + padding: 16, + alignItems: 'center', + borderTopWidth: 1, + borderTopColor: '#F0F0F0', + }, + closeButtonText: { + fontSize: 16, + fontWeight: 'bold', + color: '#2E7D4A', } }); diff --git a/components/home/StatCard.tsx b/components/home/StatCard.tsx index fbf8fa8..c9cc67f 100644 --- a/components/home/StatCard.tsx +++ b/components/home/StatCard.tsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import { View, Text, StyleSheet, ViewStyle, StyleProp } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; import Animated, { useSharedValue, useAnimatedStyle, @@ -51,6 +52,13 @@ export const StatCard = ({ icon, number, label, primary, index = 0, animatedStyl transform: [{ translateY: translateY.value }] })); + const CardContainer = primary ? LinearGradient : View; + const cardProps = primary ? { + colors: ['#2E7D4A', '#4CAF50'], + start: { x: 0, y: 0 }, + end: { x: 1, y: 1 } + } : {}; + return ( - - {number} - {label} + + + {number} + {label} - {primary && ( - - - Этап: {currentMilestone.label} - {n < 365 && ( - До {nextMilestone.label}: {nextMilestone.days - n} дн. - )} - - - + {primary && ( + + + Этап: {currentMilestone.label} + {n < 365 && ( + До {nextMilestone.label}: {nextMilestone.days - n} дн. + )} + + + + - - )} + )} + ); }; @@ -83,19 +93,24 @@ const styles = StyleSheet.create({ statCard: { flex: 1, backgroundColor: 'white', - padding: 20, borderRadius: 15, - alignItems: 'center', shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3 + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.12, + shadowRadius: 8, + elevation: 4, + overflow: 'hidden' + }, + cardInternal: { + padding: 20, + alignItems: 'center', + width: '100%', + height: '100%' }, primaryStatCard: { backgroundColor: '#2E7D4A', - alignItems: 'center', - padding: 25 + shadowColor: '#2E7D4A', + shadowOpacity: 0.3, }, statNumber: { fontSize: 28, diff --git a/package-lock.json b/package-lock.json index a68d52f..fcaedb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,6 +95,7 @@ "lucide-react-native": "^0.475.0", "nativewind": "^4.1.23", "path-to-regexp": "^1.9.0", + "playwright-chromium": "^1.61.1", "prop-types": "^15.7.2", "querystring": "^0.2.0", "react": "19.0.0", @@ -15597,6 +15598,34 @@ "node": ">=4" } }, + "node_modules/playwright-chromium": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-chromium/-/playwright-chromium-1.61.1.tgz", + "integrity": "sha512-YK3K8JakUv5Hq5WjY6oU/XwuD/041s9XVtoTKTrZHYRY72DOAsni/iXye7iBS8lgYWUZENt/6tBnpmhaSg72ug==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/plist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", diff --git a/package.json b/package.json index 58caaae..e153808 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sober-path", "main": "expo-router/entry", - "version": "1.13.0", + "version": "1.14.0", "scripts": { "start": "expo start", "reset-project": "node ./scripts/reset-project.js", @@ -99,6 +99,7 @@ "lucide-react-native": "^0.475.0", "nativewind": "^4.1.23", "path-to-regexp": "^1.9.0", + "playwright-chromium": "^1.61.1", "prop-types": "^15.7.2", "querystring": "^0.2.0", "react": "19.0.0", diff --git a/services/AICoachService.ts b/services/AICoachService.ts index f635ee4..a4e2b10 100644 --- a/services/AICoachService.ts +++ b/services/AICoachService.ts @@ -623,7 +623,18 @@ export class AICoachService { return success(updated); } - static async getMorningBriefing(userId: string, soberDays: number, mood: number): Promise { + static async getMorningBriefing(userId: string, soberDays: number, currentMood: number): Promise { + // Получаем историю настроения для адаптации + const memory = this.getUserMemory(userId); + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayConversations = memory.conversations.filter(c => + new Date(c.timestamp).toDateString() === yesterday.toDateString() + ); + const avgYesterdayMood = yesterdayConversations.length > 0 + ? yesterdayConversations.reduce((acc, curr) => acc + curr.userMood, 0) / yesterdayConversations.length + : 3; + const allQuickTips = [ "Пейте больше воды при появлении тяги", "Сделайте 10 глубоких вдохов при стрессе", @@ -653,13 +664,16 @@ export class AICoachService { let motivation = "Каждый день без алкоголя делает вас сильнее и свободнее."; let focus = "Сохранение спокойствия и осознанности"; - if (mood <= 2) { - title = "Поддержка в трудный день"; + if (currentMood <= 2 || avgYesterdayMood <= 2) { + title = currentMood <= 2 ? "Поддержка в трудный день" : "Новый день — новые возможности"; plan = [ "Избегать триггерных мест и людей", "Слушать успокаивающее SOS-аудио", "Записать свои чувства в дневник" ]; + motivation = avgYesterdayMood <= 2 + ? "Вчера было непросто, но вы справились. Сегодня — чистый лист." + : "Я рядом, чтобы помочь вам пройти через этот день."; focus = "Эмоциональная безопасность"; } diff --git a/services/articlesDatabase.ts b/services/articlesDatabase.ts index 234e914..46f23e9 100644 --- a/services/articlesDatabase.ts +++ b/services/articlesDatabase.ts @@ -1,5 +1,11 @@ // База данных образовательных статей - Полностью обновленная версия с 50+ уникальными статьями +export interface QuizQuestion { + question: string; + options: string[]; + correctAnswer: number; +} + export interface Article { id: string; title: string; @@ -14,6 +20,7 @@ export interface Article { relatedArticles?: string[]; author?: string; lastUpdated?: string; + quiz?: QuizQuestion[]; } const generateArticleId = (index: number): string => `article-${String(index).padStart(3, '0')}`; @@ -30,7 +37,19 @@ export const articlesDatabase: Article[] = [ content: `Алкоголь оказывает комплексное воздействие на мозг, затрагивая множество нейромедиаторных систем и изменяя структуру нейронных связей. ГАМК-система усиливается, глутамат блокируется, дофамин стимулируется искусственно. При регулярном употреблении развивается толерантность, а при отмене - опасный синдром отмены. Хорошая новость: мозг обладает нейропластичностью и способен восстанавливаться. Первые улучшения заметны через 2 недели трезвости, значительное восстановление когнитивных функций происходит через 3-6 месяцев, а структурное восстановление серого вещества - через 6-12 месяцев.`, tags: ['нейробиология', 'дофамин', 'мозг', 'восстановление'], icon: 'psychology', - color: '#9C27B0' + color: '#9C27B0', + quiz: [ + { + question: 'Как называется способность мозга формировать новые нейронные связи?', + options: ['Нейропластичность', 'Нейродегенерация', 'Нейросенсорика', 'Нейрофиброма'], + correctAnswer: 0 + }, + { + question: 'Через какое время обычно происходит значительное восстановление когнитивных функций?', + options: ['2 дня', '2 недели', '3-6 месяцев', '5 лет'], + correctAnswer: 2 + } + ] }, { @@ -161,7 +180,19 @@ export const articlesDatabase: Article[] = [ content: `Триггер - стимул, вызывающий тягу к алкоголю. Типы триггеров: Внешние (места - бары, дом друга; люди - компании собутыльников; время - пятница вечером; события - праздники, стресс на работе; объекты - бутылки, реклама), Внутренние (эмоции - стресс, тревога, скука, одиночество, даже радость; физические ощущения - усталость, голод; мысли - "заслужил выпить", "один раз можно"). Картирование триггеров: ведите дневник тяги (когда, где, что чувствовали, что думали), оцените силу каждого триггера 1-10, определите паттерны. План управления: избегание (на ранних этапах), "Если-То" планирование ("Если встречу друзей с алкоголем, ТО позвоню спонсору"), техники совладания для каждого триггера, создание альтернативных привычек, репетиция сложных ситуаций. Работа с внутренними триггерами: эмоции - техники регуляции (mindfulness, RAIN, CBT), физические ощущения - самопомощь (HALT проверка), мысли - когнитивная проверка. Важно: избегание - временная стратегия. Долгосрочная цель - развитие навыков справляться с триггерами.`, tags: ['триггеры', 'тяга', 'управление', 'профилактика'], icon: 'warning', - color: '#3F51B5' + color: '#3F51B5', + quiz: [ + { + question: 'Что из перечисленного является ВНУТРЕННИМ триггером?', + options: ['Бар за углом', 'Друзья по бутылке', 'Чувство одиночества', 'Реклама пива'], + correctAnswer: 2 + }, + { + question: 'Что рекомендуется делать на ранних этапах при встрече с внешними триггерами?', + options: ['Проверять силу воли', 'Избегать их', 'Смотреть им в глаза', 'Приглашать их домой'], + correctAnswer: 1 + } + ] }, { diff --git a/services/communityService.ts b/services/communityService.ts index 117d24e..7d3bda4 100644 --- a/services/communityService.ts +++ b/services/communityService.ts @@ -52,6 +52,7 @@ export interface GroupChallenge { } const POSTS_STORAGE_KEY = 'sober_path_community_posts'; +const CHALLENGES_STORAGE_KEY = 'sober_path_community_challenges'; export class CommunityService { private static userPosts: SupportPost[] = []; @@ -164,8 +165,9 @@ export class CommunityService { ]; } - static getGroupChallenges(): GroupChallenge[] { - return [ + static async getGroupChallenges(): Promise<(GroupChallenge & { isParticipating: boolean })[]> { + const participatingIds = await this.getParticipatingChallenges(); + const challenges: GroupChallenge[] = [ { id: 'c1', title: 'Марафон "Чистый Октябрь"', @@ -181,8 +183,47 @@ export class CommunityService { participants: 120, daysRemaining: 5, category: 'Привычки' + }, + { + id: 'c3', + title: 'Шаги к здоровью', + description: 'Проходим 10 000 шагов каждый день и делимся результатами.', + participants: 85, + daysRemaining: 20, + category: 'Здоровье' } ]; + + return challenges.map(c => ({ + ...c, + isParticipating: participatingIds.includes(c.id), + participants: participatingIds.includes(c.id) ? c.participants + 1 : c.participants + })); + } + + private static async getParticipatingChallenges(): Promise { + try { + const stored = await AsyncStorage.getItem(CHALLENGES_STORAGE_KEY); + return stored ? JSON.parse(stored) : []; + } catch (e) { + return []; + } + } + + static async toggleChallengeParticipation(challengeId: string): Promise { + const participating = await this.getParticipatingChallenges(); + let updated: string[]; + let joined = false; + + if (participating.includes(challengeId)) { + updated = participating.filter(id => id !== challengeId); + } else { + updated = [...participating, challengeId]; + joined = true; + } + + await AsyncStorage.setItem(CHALLENGES_STORAGE_KEY, JSON.stringify(updated)); + return joined; } static async getSupportPosts(): Promise { diff --git a/services/questService.ts b/services/questService.ts index 1fc42d6..ed21a77 100644 --- a/services/questService.ts +++ b/services/questService.ts @@ -7,17 +7,18 @@ export interface QuestMilestone { description: string; reward: string; isCompleted: boolean; + icon: string; } 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 }, + { id: '1', day: 1, title: 'Первый шаг', description: 'Первый день новой жизни. Вы приняли важное решение.', reward: 'Медаль новичка', icon: 'flare', isCompleted: false }, + { id: '3', day: 3, title: 'Установка', description: 'Три дня чистоты и ясности. Организм начинает детоксикацию.', reward: 'Бонус концентрации', icon: 'opacity', isCompleted: false }, + { id: '7', day: 7, title: 'Неделя!', description: 'Семь дней — это серьезная победа над привычкой.', reward: 'Доступ к секретному уроку', icon: 'filter-hdr', isCompleted: false }, + { id: '14', day: 14, title: 'Две недели', description: 'Привычка начинает формироваться. Вы чувствуете себя лучше.', reward: 'Эксклюзивная тема оформления', icon: 'auto-awesome', isCompleted: false }, + { id: '21', day: 21, title: 'Три недели', description: 'Организм восстанавливается на клеточном уровне.', reward: 'Значок мастера дисциплины', icon: 'psychology', isCompleted: false }, + { id: '30', day: 30, title: 'Месяц Свободы', description: '30 дней — фундамент новой жизни заложен!', reward: 'Кубок Победителя', icon: 'emoji-events', isCompleted: false }, ]; class QuestService { diff --git a/services/sosService.ts b/services/sosService.ts index 05ecf59..7556b20 100644 --- a/services/sosService.ts +++ b/services/sosService.ts @@ -139,5 +139,108 @@ export const sosPractices: SOSPractice[] = [ 'Вкус: Выпейте вкусный чай или съешьте фрукт.', 'Ваше тело — ваш союзник в обретении покоя.' ] + }, + { + id: 'quick-tip-1', + title: 'Глоток воды', + description: 'Физиологический способ переключить мозг.', + duration: '1 мин', + steps: [ + 'Выпейте стакан очень холодной воды маленькими глотками.', + 'Сосредоточьтесь на ощущении холода в горле.', + 'Это активирует блуждающий нерв и снижает интенсивность тяги.' + ] + }, + { + id: 'quick-tip-2', + title: 'Сила цитруса', + description: 'Шоковая терапия для вкусовых рецепторов.', + duration: '1 мин', + steps: [ + 'Отрежьте дольку лимона и медленно съешьте её (или просто понюхайте).', + 'Резкий кислый вкус мгновенно переключает внимание мозга с дофаминового ожидания на текущее ощущение.' + ] + }, + { + id: 'quick-tip-3', + title: 'Смена декораций', + description: 'Выход из триггерного пространства.', + duration: '2 мин', + steps: [ + 'Немедленно встаньте и перейдите в другую комнату или выйдите на улицу.', + 'Физическое перемещение помогает разорвать ассоциативную цепь триггера.' + ] + }, + { + id: 'quick-tip-4', + title: 'Музыкальный щит', + description: 'Звуковая изоляция от мыслей о срыве.', + duration: '3-5 мин', + steps: [ + 'Наденьте наушники и включите самую энергичную или, наоборот, спокойную любимую песню.', + 'Полностью погрузитесь в звуки, подпевайте или двигайтесь в такт.' + ] + }, + { + id: 'quick-tip-5', + title: 'Контраст для рук', + description: 'Температурное заземление.', + duration: '2 мин', + steps: [ + 'Подержите руки под очень холодной, а затем под теплой водой.', + 'Повторите 3 раза.', + 'Смена температурных сигналов "занимает" нервную систему, вытесняя импульс к употреблению.' + ] + }, + { + id: 'quick-tip-6', + title: 'Метод резинки', + description: 'Физическое напоминание о реальности.', + duration: '1 мин', + steps: [ + 'Носите на запястье обычную банковскую резинку.', + 'При появлении навязчивых мыслей слегка оттяните и щелкните ею по руке.', + 'Легкий болевой стимул возвращает внимание в тело и прерывает транс тяги.' + ] + }, + { + id: 'quick-tip-7', + title: '15 приседаний', + description: 'Гормональная перезагрузка.', + duration: '2 мин', + steps: [ + 'Сделайте 15 быстрых приседаний или отжиманий.', + 'Физическая нагрузка вызывает выброс естественных эндорфинов, которые помогут мозгу справиться без суррогатов.' + ] + }, + { + id: 'quick-tip-8', + title: 'Звонок трезвости', + description: 'Социальная страховка.', + duration: '5 мин', + steps: [ + 'Не ждите, пока тяга станет невыносимой. Позвоните другу, который знает о вашем пути.', + 'Просто скажите: "Мне сейчас трудно". Проговаривание вслух лишает тягу её тайной силы.' + ] + }, + { + id: 'quick-tip-9', + title: 'Счет до 100', + description: 'Ментальная перегрузка.', + duration: '3 мин', + steps: [ + 'Начните считать от 100 в обратном порядке, вычитая по 7 (100, 93, 86...).', + 'Эта задача требует концентрации префронтальной коры, что автоматически подавляет импульсивные центры мозга.' + ] + }, + { + id: 'quick-tip-10', + title: 'Твое "Зачем"', + description: 'Возврат к ценностям.', + duration: '2 мин', + steps: [ + 'Посмотрите на фото того, что вам дорого, или прочитайте список своих главных целей.', + 'Напомните себе: "Эта тяга длится 15 минут, а последствия срыва — месяцы. Я выбираю себя".' + ] } ];