feat: add expo mobile application source code

This commit is contained in:
2026-05-21 16:06:35 +07:00
parent 76d7a5c5c6
commit 0c65a7811b
77 changed files with 20356 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
import React, { useState, useEffect, useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, TextInput, ScrollView, Platform } from 'react-native';
import { Feather } from '@expo/vector-icons';
import { useAppTheme } from '../../context/ThemeContext';
import { useToast } from '../../context/ToastContext';
import { useAppConfig } from '../../context/ConfigContext';
import { AppScreen } from '../../components/AppScreen';
import { AISectionHeader, AISkeleton, AIPressable } from '../../components/UI';
import { useTranslation } from '../../context/LanguageContext';
import { MOCK_FAQS } from '../../constants/mocks';
import { PALETTE } from '../../constants/theme';
import * as Haptics from 'expo-haptics';
const getTopics = (t: any) => [
{ id: '1', name: t.web || 'Web', icon: 'book-open' },
{ id: '2', name: t.account || 'Account', icon: 'user' },
{ id: '3', name: t.billing || 'Billing', icon: 'credit-card' },
{ id: '4', name: t.system || 'System', icon: 'cpu' },
];
// Mock data moved to constants/mocks.ts
export default function HelpScreen() {
const { colors, isDark } = useAppTheme();
const { showToast } = useToast();
const { config } = useAppConfig();
const { t } = useTranslation();
const topics = useMemo(() => {
if (config?.support_social?.help_topics_json && Array.isArray(config.support_social.help_topics_json)) {
return config.support_social.help_topics_json;
}
return getTopics(t);
}, [config?.support_social?.help_topics_json, t]);
const faqs = useMemo(() => {
if (config?.support_social?.faq_json && Array.isArray(config.support_social.faq_json)) {
return config.support_social.faq_json.map((f, i) => ({ id: String(i+1), ...f }));
}
return MOCK_FAQS;
}, [config?.support_social?.faq_json]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
useEffect(() => {
const timer = setTimeout(() => setLoading(false), 1500);
return () => clearTimeout(timer);
}, []);
const handleContactSupport = (type: 'whatsapp' | 'email') => {
const contact = type === 'whatsapp' ? config?.support_social?.support_whatsapp : config?.support_social?.support_email;
if (contact) {
const url = type === 'whatsapp' ? `https://wa.me/${contact}` : `mailto:${contact}`;
require('react-native').Linking.openURL(url).catch(() => {
showToast(`Failed to open ${type}`, 'error');
});
} else {
showToast('Support contact not available', 'info');
}
};
const cardBg = colors.surface;
const border = colors.border;
const subText = colors.textSecondary;
const renderTopic = (topic: any) => {
return (
<AIPressable
key={topic.id}
onPress={() => showToast(`Opening ${topic.name} topics`, 'info')}
style={styles.topicWrapper}
containerStyle={{ flex: 1 }}
>
<View style={[styles.topicCard, { backgroundColor: cardBg, borderColor: border }]}>
<Feather name={topic.icon as any} size={24} color={isDark ? colors.primary : colors.secondary} />
<Text style={[styles.topicName, { color: colors.text }]} numberOfLines={1}>{topic.name}</Text>
</View>
</AIPressable>
);
};
const renderSkeleton = () => (
<View style={{ paddingHorizontal: 24, paddingTop: 56 }}>
<AISkeleton width={220} height={32} style={{ marginBottom: 12 }} />
<AISkeleton width={160} height={14} style={{ marginBottom: 32 }} />
<AISkeleton width="100%" height={56} radius={14} style={{ marginBottom: 32 }} />
<View style={{ flexDirection: 'row', gap: 12 }}>
{[1, 2, 3, 4].map(i => <AISkeleton key={i} width="22%" height={90} radius={20} />)}
</View>
</View>
);
if (loading) return <AppScreen scrollable={false}>{renderSkeleton()}</AppScreen>;
return (
<AppScreen>
<View>
{/* Header */}
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text }]}>{t.supportCenter || 'Support Center'}</Text>
<Text style={[styles.subtitle, { color: subText }]}>
{t.helpSubtitle || 'Find answers'}
</Text>
</View>
{/* Search */}
<View style={styles.searchSection}>
<View style={[styles.searchBox, { backgroundColor: cardBg, borderColor: border }]}>
<Feather name="search" size={18} color={colors.textPlaceholder} />
<TextInput
placeholder={t.searchDoc || "Search documentation..."}
placeholderTextColor={isDark ? '#444' : '#BBBBBB'}
style={[styles.searchInput, { color: colors.text }]}
value={search}
onChangeText={setSearch}
/>
</View>
</View>
{/* Topics Grid: 2 Columns FULL */}
<AISectionHeader title={t.browseTopics || "Browse Topics"} />
<View style={styles.topicGrid}>
<View style={styles.topicRow}>
{renderTopic(topics[0])}
{renderTopic(topics[1])}
</View>
<View style={styles.topicRow}>
{renderTopic(topics[2])}
{renderTopic(topics[3])}
</View>
</View>
{/* FAQs */}
<AISectionHeader title={t.faqTitle || "Frequently Asked Questions (FAQ)"} />
<View style={styles.faqList}>
{faqs.map((faq: any) => (
<AIPressable key={faq.id} style={[styles.faqCard, { backgroundColor: cardBg, borderColor: border }]}>
<View style={styles.faqRow}>
<View style={styles.faqIconBox}>
<Feather name="help-circle" size={18} color={colors.primary} />
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.question, { color: colors.text }]}>{faq.q}</Text>
<Text style={[styles.answer, { color: subText }]}>{faq.a}</Text>
</View>
<Feather name="chevron-right" size={16} color={subText} />
</View>
</AIPressable>
))}
</View>
{/* Contact Footer */}
<View style={styles.footerRow}>
<AIPressable style={styles.supportBtn} onPress={() => handleContactSupport('whatsapp')}>
<View style={[styles.contactCard, { backgroundColor: '#1A1A1A' }]}>
<View style={[styles.contactIcon, { backgroundColor: '#25D36620' }]}>
<Feather name="message-circle" size={20} color="#25D366" />
</View>
<Text style={styles.contactLabel}>{t.whatsapp || 'WhatsApp'}</Text>
</View>
</AIPressable>
<AIPressable style={styles.supportBtn} onPress={() => handleContactSupport('email')}>
<View style={[styles.contactCard, { backgroundColor: '#1A1A1A' }]}>
<View style={[styles.contactIcon, { backgroundColor: colors.primary + '20' }]}>
<Feather name="mail" size={20} color={colors.primary} />
</View>
<Text style={styles.contactLabel}>{t.emailSupport || 'Email Support'}</Text>
</View>
</AIPressable>
</View>
<View style={{ height: 110 }} />
</View>
</AppScreen>
);
}
const styles = StyleSheet.create({
header: { paddingHorizontal: 24, paddingTop: 20, marginBottom: 20 },
title: { fontSize: 32, fontFamily: 'Outfit_800ExtraBold', letterSpacing: -0.5 },
subtitle: { fontSize: 13, fontFamily: 'Outfit_400Regular', marginTop: 4 },
searchSection: { paddingHorizontal: 24, marginBottom: 28 },
searchBox: { flexDirection: 'row', alignItems: 'center', height: 56, borderRadius: 16, borderWidth: 1, paddingHorizontal: 16, gap: 12 },
searchInput: { flex: 1, fontSize: 15, fontFamily: 'Outfit_500Medium' },
topicGrid: { paddingHorizontal: 24, gap: 12, marginBottom: 24 },
topicRow: { flexDirection: 'row', gap: 12 },
topicWrapper: { flex: 1 },
topicCard: { height: 100, borderRadius: 20, borderWidth: 1, alignItems: 'center', justifyContent: 'center', gap: 10 },
topicName: { fontSize: 13, fontFamily: 'Outfit_700Bold' },
faqList: { paddingHorizontal: 24, gap: 12 },
faqCard: { borderRadius: 24, borderWidth: 1, padding: 16 },
faqRow: { flexDirection: 'row', alignItems: 'center', gap: 14 },
faqIconBox: { width: 36, height: 36, borderRadius: 10, backgroundColor: '#C6F13520', alignItems: 'center', justifyContent: 'center' },
question: { fontSize: 14, fontFamily: 'Outfit_700Bold', marginBottom: 2 },
answer: { fontSize: 12, fontFamily: 'Outfit_400Regular', lineHeight: 18 },
footerRow: { flexDirection: 'row', paddingHorizontal: 24, gap: 12, marginTop: 32 },
supportBtn: { flex: 1 },
contactCard: { height: 120, borderRadius: 24, alignItems: 'center', justifyContent: 'center', gap: 12 },
contactIcon: { width: 44, height: 44, borderRadius: 14, alignItems: 'center', justifyContent: 'center' },
contactLabel: { color: '#FFFFFF', fontSize: 13, fontFamily: 'Outfit_700Bold' },
});