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