feat: add expo mobile application source code
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
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 },
|
||||
});
|
||||
Reference in New Issue
Block a user