Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 и обновить скриншоты.
90 changes: 89 additions & 1 deletion app/(tabs)/community.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -195,6 +195,7 @@ export default function CommunityPage() {
const [posts, setPosts] = useState<SupportPost[]>([]);
const [expertQA, setExpertQA] = useState<ExpertQA[]>([]);
const [communityGoals, setCommunityGoals] = useState<CommunityGoal[]>([]);
const [groupChallenges, setGroupChallenges] = useState<GroupChallenge[]>([]);
const [circles, setCircles] = useState<any[]>([]);
const [selectedCircle, setSelectedCircle] = useState('all');
const [isModalVisible, setIsModalVisible] = useState(false);
Expand All @@ -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();
Expand Down Expand Up @@ -314,6 +316,37 @@ export default function CommunityPage() {

return (
<View>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Групповые челленджи</Text>
</View>

<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.goalsContainer}
>
{isLoading ? (
[1, 2].map(i => <Skeleton key={i} width={250} height={120} borderRadius={16} />)
) : (
groupChallenges.map(challenge => (
<TouchableOpacity key={challenge.id} style={styles.challengeCard}>
<View style={styles.challengeHeader}>
<View style={styles.challengeBadge}>
<Text style={styles.challengeBadgeText}>{challenge.category}</Text>
</View>
<Text style={styles.challengeDays}>осталось {challenge.daysRemaining} дн.</Text>
</View>
<Text style={styles.challengeTitle}>{challenge.title}</Text>
<Text style={styles.challengeDesc} numberOfLines={2}>{challenge.description}</Text>
<View style={styles.challengeFooter}>
<MaterialIcons name="people" size={16} color="#666" />
<Text style={styles.challengeParticipants}>{challenge.participants} участников</Text>
</View>
</TouchableOpacity>
))
)}
</ScrollView>

<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Цели сообщества</Text>
</View>
Expand Down Expand Up @@ -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,
Expand Down
80 changes: 78 additions & 2 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -133,6 +136,9 @@ function HomePage() {
const [dailyTip, setDailyTip] = useState<RecoveryTip | null>(null);
const [weeklyRoadmap, setWeeklyRoadmap] = useState<WeeklyRoadmap | null>(null);
const [isUpdatingRoadmap, setIsUpdatingRoadmap] = useState(false);
const [questMilestones, setQuestMilestones] = useState<QuestMilestone[]>([]);
const [showBriefing, setShowBriefing] = useState(false);
const [morningBriefing, setMorningBriefing] = useState<MorningBriefing | null>(null);
const [alertConfig, setAlertConfig] = useState<{
visible: boolean;
title: string;
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -357,6 +383,8 @@ function HomePage() {
<StatCard icon="check-circle" number={totalSoberDays} label={t('home.totalSober')} />
</View>

<QuestMap milestones={questMilestones} currentSoberDays={soberDays} />

{weeklyRoadmap && (
<Link href="/ai-coach" asChild>
<TouchableOpacity style={styles.roadmapWidget}>
Expand Down Expand Up @@ -703,6 +731,54 @@ function HomePage() {
</View>
</Modal>

{/* Morning Briefing Modal */}
<Modal
visible={showBriefing}
transparent={true}
animationType="slide"
onRequestClose={() => setShowBriefing(false)}
>
<View style={styles.modalOverlay}>
<View style={[styles.moodModalContent, { maxHeight: '80%' }]}>
<View style={{ alignItems: 'center', marginBottom: 15 }}>
<MaterialIcons name="wb-sunny" size={40} color="#FFB300" />
<Text style={styles.modalTitle}>{morningBriefing?.title}</Text>
</View>

<ScrollView showsVerticalScrollIndicator={false}>
<Text style={styles.briefingFocus}>Установка дня: {morningBriefing?.focus}</Text>

<Text style={styles.briefingSectionTitle}>Ваш план:</Text>
{morningBriefing?.plan.map((item, idx) => (
<View key={idx} style={styles.briefingItem}>
<MaterialIcons name="check" size={18} color="#2E7D4A" />
<Text style={styles.briefingText}>{item}</Text>
</View>
))}

<Text style={styles.briefingSectionTitle}>Советы:</Text>
{morningBriefing?.quickTips.map((tip, idx) => (
<View key={idx} style={styles.briefingItem}>
<MaterialIcons name="lightbulb" size={18} color="#FFB300" />
<Text style={styles.briefingText}>{tip}</Text>
</View>
))}

<View style={styles.motivationQuoteBox}>
<Text style={styles.briefingMotivation}>"{morningBriefing?.motivation}"</Text>
</View>
</ScrollView>

<TouchableOpacity
style={[styles.modalButton, styles.confirmButton, { marginTop: 20 }]}
onPress={() => setShowBriefing(false)}
>
<Text style={styles.confirmButtonText}>Понятно</Text>
</TouchableOpacity>
</View>
</View>
</Modal>

{Platform.OS === 'web' && (
<Modal visible={alertConfig.visible} transparent animationType="fade">
<View style={styles.webAlertOverlay}>
Expand Down
107 changes: 107 additions & 0 deletions components/QuestMap.tsx
Original file line number Diff line number Diff line change
@@ -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<QuestMapProps> = ({ milestones, currentSoberDays }) => {
const { colors } = useAppTheme();

return (
<View style={styles.container}>
<Text style={[styles.title, { color: colors.text }]}>Квест: Первые 30 дней</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
{milestones.map((milestone, index) => {
const isCompleted = currentSoberDays >= milestone.day;
const isCurrent = !isCompleted && (index === 0 || currentSoberDays >= milestones[index - 1].day);

return (
<View key={milestone.id} style={styles.milestoneWrapper}>
<View style={[
styles.node,
{ backgroundColor: isCompleted ? colors.primary : colors.card },
isCurrent && { borderColor: colors.primary, borderWidth: 2 }
]}>
<Ionicons
name={isCompleted ? "checkmark-circle" : "lock-closed"}
size={24}
color={isCompleted ? "#fff" : colors.textSecondary}
/>
</View>
<Text style={[styles.dayText, { color: colors.text }]}>{milestone.day} день</Text>
<Text style={[styles.nodeTitle, { color: colors.textSecondary }]} numberOfLines={1}>
{milestone.title}
</Text>

{index < milestones.length - 1 && (
<View style={[
styles.connector,
{ backgroundColor: isCompleted ? colors.primary : colors.border }
]} />
)}
</View>
);
})}
</ScrollView>
</View>
);
};

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,
}
});
Loading
Loading