251 lines
11 KiB
TypeScript
251 lines
11 KiB
TypeScript
import React, { useState, useMemo, useEffect } from 'react';
|
|
import {
|
|
View, Text, StyleSheet, TouchableOpacity, Image,
|
|
FlatList, Platform, StatusBar, Dimensions
|
|
} from 'react-native';
|
|
import { Feather } from '@expo/vector-icons';
|
|
import { useRouter } from 'expo-router';
|
|
import { useAuth } from '../../context/AuthContext';
|
|
import { useAppTheme } from '../../context/ThemeContext';
|
|
import { useToast } from '../../context/ToastContext';
|
|
import { AppScreen } from '../../components/AppScreen';
|
|
import { AISectionHeader, AISkeleton, AIPressable } from '../../components/UI';
|
|
import { useTranslation } from '../../context/LanguageContext';
|
|
import { useAppConfig } from '../../context/ConfigContext';
|
|
import { MOCK_ARTICLES } from '../../constants/mocks';
|
|
import * as Haptics from 'expo-haptics';
|
|
|
|
|
|
const getQuickActions = (t: any) => {
|
|
return [
|
|
{ id: '1', name: t('account') || 'Account', icon: 'user', dark: true },
|
|
{ id: '2', name: t('subscription') || 'Subscription', icon: 'credit-card', dark: false },
|
|
{ id: '3', name: t('system') || 'System', icon: 'cpu', dark: true },
|
|
{ id: '4', name: t('explore') || 'Explore', icon: 'compass', dark: false },
|
|
];
|
|
};
|
|
|
|
const getCategories = (t: any) => [
|
|
{ id: '1', name: t('all') || 'All' },
|
|
{ id: '2', name: 'LLM' },
|
|
{ id: '3', name: 'Robotics' },
|
|
{ id: '4', name: 'Health' },
|
|
{ id: '5', name: 'Coding' },
|
|
];
|
|
|
|
// Mock data moved to constants/mocks.ts
|
|
|
|
export default function Dashboard() {
|
|
const { user } = useAuth();
|
|
const { colors, isDark } = useAppTheme();
|
|
const { showToast } = useToast();
|
|
const { t } = useTranslation();
|
|
const { config } = useAppConfig();
|
|
const router = useRouter();
|
|
|
|
const quickActions = useMemo(() => getQuickActions(t), [t]);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const categories = useMemo(() => {
|
|
if (config?.features?.dashboard_categories) {
|
|
return config.features.dashboard_categories.split(',').map((name, index) => ({
|
|
id: String(index + 1),
|
|
name: name.trim()
|
|
}));
|
|
}
|
|
return getCategories(t);
|
|
}, [config?.features?.dashboard_categories, t]);
|
|
|
|
const [activeCategory, setActiveCategory] = useState(t('all') || 'All');
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setLoading(false), 1500);
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
const handleAction = (name: string) => {
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
|
showToast(`Opening ${name}`, 'info');
|
|
};
|
|
|
|
const filteredArticles = useMemo(
|
|
() => MOCK_ARTICLES.filter(a => activeCategory === 'All' || a.category === activeCategory),
|
|
[activeCategory]
|
|
);
|
|
|
|
const cardBg = colors.surface;
|
|
const cardBorder = colors.border;
|
|
const subText = colors.textSecondary;
|
|
|
|
const renderHeader = () => (
|
|
<View style={styles.headerContent}>
|
|
{/* ── Greeting row ── */}
|
|
<View style={styles.headerTop}>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={[styles.greeting, { color: colors.textSecondary }]}>{t('halo') || 'Good morning'} 👋</Text>
|
|
<Text style={[styles.welcomeText, { color: colors.text }]}>{(user?.name || 'Alex').split(' ')[0]}</Text>
|
|
</View>
|
|
<TouchableOpacity onPress={() => router.push('/(tabs)/explore')}>
|
|
<Image
|
|
source={{ uri: user?.avatar || `https://i.pravatar.cc/150?u=1` }}
|
|
style={[styles.avatar, { borderColor: colors.primary }]}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* ── Highlight card ── */}
|
|
<AIPressable onPress={() => handleAction(t('getHelp') || 'Support')} style={styles.highlightPressable}>
|
|
<View style={[styles.highlightCard, { backgroundColor: '#1A1A1A' }]}>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={styles.highlightLabel}>{t('systemSupport') || 'System Support'}</Text>
|
|
<Text style={styles.highlightValue}>{t('instantHelp') || 'Instant Help 24/7'}</Text>
|
|
<View style={[styles.limeBtn, { backgroundColor: colors.primary }]}>
|
|
<Text style={styles.limeBtnText}>{t('getHelp') || 'Get Help'}</Text>
|
|
<Feather name="arrow-right" size={14} color="#1A1A1A" style={{ marginLeft: 6 }} />
|
|
</View>
|
|
</View>
|
|
<View style={[styles.highlightIcon, { backgroundColor: colors.primary + '20' }]}>
|
|
<Feather name="shield" size={38} color={colors.primary} />
|
|
</View>
|
|
</View>
|
|
</AIPressable>
|
|
|
|
{/* ── Quick action grid ── */}
|
|
<AISectionHeader title={t('quickActions') || "Quick Actions"} />
|
|
<View style={styles.quickGrid}>
|
|
<View style={styles.actionRow}>
|
|
{renderAction(quickActions[0], false)}
|
|
{renderAction(quickActions[1], true)}
|
|
</View>
|
|
<View style={styles.actionRow}>
|
|
{renderAction(quickActions[2], true)}
|
|
{renderAction(quickActions[3], false)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* ── Categories ── */}
|
|
<AISectionHeader title={t('categories') || "Categories"} />
|
|
<FlatList
|
|
data={categories}
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerStyle={styles.categoryList}
|
|
keyExtractor={item => item.id}
|
|
renderItem={({ item }) => {
|
|
const isActive = activeCategory === item.name;
|
|
return (
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
Haptics.selectionAsync();
|
|
setActiveCategory(item.name);
|
|
}}
|
|
style={[
|
|
styles.categoryPill,
|
|
{
|
|
backgroundColor: isActive ? (isDark ? colors.primary : '#1A1A1A') : (isDark ? '#2A2A2A' : '#FFFFFF'),
|
|
borderColor: isActive ? 'transparent' : (isDark ? '#3A3A3C' : '#EEEEEE'),
|
|
}
|
|
]}
|
|
>
|
|
<Text style={[styles.categoryText, { color: isActive ? (isDark ? '#1A1A1A' : '#FFFFFF') : (isDark ? '#9B9B9B' : '#6B6B6B') }]}>
|
|
{item.name}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
);
|
|
}}
|
|
/>
|
|
<AISectionHeader title={t('latestDiscoveries') || "Latest Discoveries"} style={{ marginTop: 20 }} />
|
|
</View>
|
|
);
|
|
|
|
const renderAction = (item: any, isDarkCard: boolean) => {
|
|
const bg = isDarkCard ? (isDark ? '#2A2A2A' : '#1A1A1A') : colors.primary;
|
|
const iconColor = isDarkCard ? (isDark ? colors.primary : '#FFFFFF') : '#1A1A1A';
|
|
const textColor = isDarkCard ? '#FFFFFF' : '#1A1A1A';
|
|
|
|
return (
|
|
<AIPressable
|
|
key={item.id}
|
|
onPress={() => handleAction(item.name)}
|
|
style={styles.actionCardWrapper}
|
|
containerStyle={styles.actionCardInner}
|
|
>
|
|
<View style={[styles.innerContent, { backgroundColor: bg, borderColor: isDark ? '#333' : 'transparent' }]}>
|
|
<Feather name={item.icon} size={24} color={iconColor} />
|
|
<Text style={[styles.actionName, { color: textColor }]} numberOfLines={1}>{item.name}</Text>
|
|
</View>
|
|
</AIPressable>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<AppScreen scrollable={false}>
|
|
{loading ? (
|
|
<View style={{ padding: 24 }}><AISkeleton width="100%" height={200} radius={24} /></View>
|
|
) : (
|
|
<FlatList
|
|
data={filteredArticles}
|
|
keyExtractor={item => item.id}
|
|
ListHeaderComponent={renderHeader}
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={styles.scrollContent}
|
|
renderItem={({ item }) => (
|
|
<AIPressable
|
|
onPress={() => router.push({ pathname: '/detail/[id]' as any, params: { ...item, id: item.id } })}
|
|
style={styles.feedPressable}
|
|
>
|
|
<View style={[styles.feedCardInner, { backgroundColor: cardBg, borderColor: cardBorder }]}>
|
|
<Image source={{ uri: item.img }} style={styles.cardImg} />
|
|
<View style={{ flex: 1, marginLeft: 14 }}>
|
|
<View style={[styles.cardCatWrap, { backgroundColor: colors.primary + '20' }]}><Text style={[styles.cardCat, { color: colors.primary }]}>{item.category}</Text></View>
|
|
<Text style={[styles.cardTitle, { color: colors.text }]} numberOfLines={2}>{item.title}</Text>
|
|
<Text style={[styles.cardAuthor, { color: subText }]}>{item.author}</Text>
|
|
</View>
|
|
<Feather name="chevron-right" size={18} color={isDark ? '#444' : '#CCCCCC'} />
|
|
</View>
|
|
</AIPressable>
|
|
)}
|
|
ListFooterComponent={<View style={{ height: 100 }} />}
|
|
/>
|
|
)}
|
|
</AppScreen>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
scrollContent: { paddingBottom: 20 },
|
|
headerContent: { paddingTop: 10 },
|
|
headerTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 24, marginBottom: 20 },
|
|
greeting: { fontSize: 13, fontFamily: 'Outfit_400Regular' },
|
|
welcomeText: { fontSize: 32, fontFamily: 'Outfit_800ExtraBold', marginTop: 2 },
|
|
avatar: { width: 48, height: 48, borderRadius: 24, borderWidth: 2.5 },
|
|
|
|
highlightPressable: { marginHorizontal: 24, marginBottom: 24 },
|
|
highlightCard: { borderRadius: 24, padding: 22, flexDirection: 'row', alignItems: 'center', elevation: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.15, shadowRadius: 20 },
|
|
highlightLabel: { color: '#6B6B6B', fontFamily: 'Outfit_500Medium', fontSize: 11, textTransform: 'uppercase' },
|
|
highlightValue: { color: '#FFFFFF', fontFamily: 'Outfit_800ExtraBold', fontSize: 22, marginTop: 4, marginBottom: 16 },
|
|
limeBtn: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 12 },
|
|
limeBtnText: { color: '#1A1A1A', fontFamily: 'Outfit_700Bold', fontSize: 13 },
|
|
highlightIcon: { width: 68, height: 68, borderRadius: 20, alignItems: 'center', justifyContent: 'center', marginLeft: 16 },
|
|
|
|
quickGrid: { paddingHorizontal: 24, gap: 12 },
|
|
actionRow: { flexDirection: 'row', gap: 12, marginBottom: 12 },
|
|
actionCardWrapper: { flex: 1 },
|
|
actionCardInner: { flex: 1 },
|
|
innerContent: { height: 94, borderRadius: 20, borderWidth: 1, padding: 16, justifyContent: 'space-between' },
|
|
actionName: { fontSize: 14, fontFamily: 'Outfit_700Bold' },
|
|
|
|
categoryList: { paddingHorizontal: 24, paddingBottom: 4 },
|
|
categoryPill: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 12, marginRight: 10, borderWidth: 1 },
|
|
categoryText: { fontSize: 13, fontFamily: 'Outfit_600SemiBold' },
|
|
|
|
feedPressable: { marginHorizontal: 24, marginBottom: 10 },
|
|
feedCardInner: { flexDirection: 'row', padding: 12, alignItems: 'center', borderRadius: 20, borderWidth: 1 },
|
|
cardImg: { width: 70, height: 70, borderRadius: 14 },
|
|
cardCatWrap: { alignSelf: 'flex-start', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 6, marginBottom: 6 },
|
|
cardCat: { fontSize: 9, fontFamily: 'Outfit_800ExtraBold', textTransform: 'uppercase' },
|
|
cardTitle: { fontSize: 14, fontFamily: 'Outfit_700Bold', lineHeight: 18 },
|
|
cardAuthor: { fontSize: 11, fontFamily: 'Outfit_400Regular', marginTop: 4 },
|
|
});
|