Загрузка данных
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, SafeAreaView, TouchableOpacity, FlatList, Alert, Image, Modal } from 'react-native';
import Svg, { Polygon, Text as SvgText } from 'react-native-svg';
import * as ImagePicker from 'expo-image-picker';
import AsyncStorage from '@react-native-async-storage/async-storage';
export default function App() {
const [isDark, setIsDark] = useState(true);
const [themeColor, setThemeColor] = useState('#0f0');
const [showSettings, setShowSettings] = useState(false);
const [avatar, setAvatar] = useState(null);
const [activeTab, setActiveTab] = useState('Ежедневные');
const [stats, setStats] = useState({ Сила: 20, Интеллект: 20, Дисциплина: 20, Амбиции: 20, Отношения: 20, Менталка: 20 });
const pool = {
'Ежедневные': Array.from({length: 7}, (_, i) => ({t: `Задача ${i+1}`, d: '10 мин', s: 'Сила'})),
'Еженедельные': Array.from({length: 10}, (_, i) => ({t: `Недельная ${i+1}`, d: '30 мин', s: 'Дисциплина'})),
'Ежемесячные': Array.from({length: 15}, (_, i) => ({t: `Месячная ${i+1}`, d: '1 час', s: 'Интеллект'}))
};
const [quests, setQuests] = useState([]);
useEffect(() => {
const loadData = async () => {
const savedStats = await AsyncStorage.getItem('stats');
const savedAvatar = await AsyncStorage.getItem('avatar');
if (savedStats) setStats(JSON.parse(savedStats));
if (savedAvatar) setAvatar(savedAvatar);
const allQuests = Object.keys(pool).flatMap(type => pool[type].map(item => ({
id: Math.random(), title: item.t, desc: item.d, type, stat: item.s, status: null, timeLeft: 86400
})));
setQuests(allQuests);
};
loadData();
}, []);
useEffect(() => {
AsyncStorage.setItem('stats', JSON.stringify(stats));
if (avatar) AsyncStorage.setItem('avatar', avatar);
}, [stats, avatar]);
useEffect(() => {
const timer = setInterval(() => {
setQuests(prev => prev.map(q => q.timeLeft > 0 ? {...q, timeLeft: q.timeLeft - 1} : {...q, timeLeft: 86400, status: null}));
}, 1000);
return () => clearInterval(timer);
}, []);
const handleAction = (id, type) => {
setQuests(prev => prev.map(q => q.id === id ? {...q, status: type} : q));
setStats(s => ({...s, [quests.find(x => x.id === id).stat]: Math.max(0, Math.min(type === 'done' ? s[quests.find(x => x.id === id).stat] + 5 : s[quests.find(x => x.id === id).stat] - 5, 100))}));
};
const getPoints = (r, vals) => {
return Object.keys(vals).map((k, i) => {
const a = (Math.PI * 2 * i) / 6 - Math.PI / 2;
const v = (vals[k] / 100) * r;
return `${150 + v * Math.cos(a)},${150 + v * Math.sin(a)}`;
}).join(" ");
};
return (
<SafeAreaView style={[styles.container, { backgroundColor: isDark ? '#000' : '#fff' }]}>
<TouchableOpacity style={styles.settingsBtn} onPress={() => setShowSettings(true)}>
<Text style={{fontSize: 24}}>⚙️</Text>
</TouchableOpacity>
<Modal visible={showSettings} transparent={true}>
<View style={styles.modal}>
<TouchableOpacity onPress={() => setIsDark(!isDark)} style={styles.menuBtn}>
<Text style={{color:'#fff'}}>Переключить тему</Text>
</TouchableOpacity>
<View style={{flexDirection:'row'}}>
{['#0f0', '#f00', '#0ff', '#ffd700'].map(c => (
<TouchableOpacity key={c} onPress={() => setThemeColor(c)} style={[styles.colorBtn, {backgroundColor: c}]}/>
))}
</View>
<TouchableOpacity onPress={() => setStats({Сила:20, Интеллект:20, Дисциплина:20, Амбиции:20, Отношения:20, Менталка:20})}>
<Text style={{color:'red', marginTop: 10}}>СБРОС СТАТ</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setShowSettings(false)}>
<Text style={{color:'#fff', marginTop:20}}>Назад</Text>
</TouchableOpacity>
</View>
</Modal>
<TouchableOpacity onPress={async () => { let r = await ImagePicker.launchImageLibraryAsync(); if(!r.canceled) setAvatar(r.assets[0].uri); }}>
<View style={[styles.avatarBorder, {borderColor: themeColor}]}>
<Image source={avatar ? {uri: avatar} : {uri: 'https://via.placeholder.com/100'}} style={styles.avatar}/>
</View>
</TouchableOpacity>
<View style={styles.radarContainer}>
<Svg height={300} width={300}>
{[80, 53, 26].map(r => <Polygon key={r} points={getPoints(r, {1:100,2:100,3:100,4:100,5:100,6:100})} fill="none" stroke={isDark ? "#555" : "#ccc"} strokeWidth="1"/>)}
<Polygon points={getPoints(80, stats)} fill={themeColor} fillOpacity="0.4" stroke={themeColor} strokeWidth="2"/>
{Object.keys(stats).map((k, i) => {
const a = (Math.PI * 2 * i) / 6 - Math.PI / 2;
return <SvgText key={k} x={150 + 115 * Math.cos(a)} y={150 + 115 * Math.sin(a)} fill={isDark ? '#fff' : '#000'} fontSize="12">{`${k} (${stats[k]}%)`}</SvgText>
})}
</Svg>
</View>
<View style={styles.tabBar}>
{['Ежедневные', 'Еженедельные', 'Ежемесячные'].map(t => (
<TouchableOpacity key={t} onPress={() => setActiveTab(t)}>
<Text style={{color: activeTab === t ? themeColor : '#888'}}>{t}</Text>
</TouchableOpacity>
))}
</View>
<FlatList
data={quests.filter(q => q.type === activeTab)}
renderItem={({item}) => (
<View style={[styles.card, {backgroundColor: item.status === 'done' ? '#060' : item.status === 'fail' ? '#600' : '#333'}]}>
<Text style={{color:'#fff'}}>{item.title} ({item.desc})</Text>
<View style={{flexDirection:'row'}}>
<TouchableOpacity disabled={!!item.status} onPress={() => handleAction(item.id, 'done')}><Text>✅</Text></TouchableOpacity>
<TouchableOpacity disabled={!!item.status} onPress={() => handleAction(item.id, 'fail')}><Text>❌</Text></TouchableOpacity>
</View>
</View>
)}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20 },
avatarBorder: { width: 80, height: 80, borderRadius: 40, borderWidth: 3, alignItems: 'center', justifyContent: 'center' },
avatar: { width: 70, height: 70, borderRadius: 35 },
radarContainer: { alignItems: 'center', marginVertical: 20 },
tabBar: { flexDirection: 'row', justifyContent: 'space-around', marginBottom: 20 },
card: { padding: 15, marginVertical: 5, borderRadius: 10, flexDirection: 'row', justifyContent: 'space-between' },
settingsBtn: { position: 'absolute', top: 50, right: 20 },
modal: { flex: 1, backgroundColor: 'rgba(0,0,0,0.8)', justifyContent: 'center', alignItems: 'center' },
colorBtn: { width: 40, height: 40, borderRadius: 20, margin: 5 },
menuBtn: { padding: 10, backgroundColor: '#333', borderRadius: 5, marginBottom: 10 }
});