Files
biiproject-kit-v1/mobile/app/(tabs)/index.tsx
T

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