feat: SAPIENS Mobile App - Initial commit
React Native mobile application for SAPIENS news platform. Consolidated all previous history into single commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
444
sapiense-ai-app/app/outlet/[id].tsx
Normal file
444
sapiense-ai-app/app/outlet/[id].tsx
Normal file
@ -0,0 +1,444 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, FlatList, ActivityIndicator, StyleSheet, SafeAreaView, Image, RefreshControl, TouchableOpacity, Modal } from 'react-native';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useOutlet, useArticlesByOutlet } from '@/hooks/useApi';
|
||||
import ArticleCard from '@/components/ArticleCard';
|
||||
import BottomTabBar from '@/components/BottomTabBar';
|
||||
import SearchModal from '@/components/SearchModal';
|
||||
import LanguageModal from '@/components/LanguageModal';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
|
||||
export default function OutletScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [activeFilter, setActiveFilter] = useState<'people' | 'topics' | 'companies'>('people');
|
||||
const [showProfileModal, setShowProfileModal] = useState(false);
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [showLanguage, setShowLanguage] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<'card' | 'list'>('card');
|
||||
const { language, setLanguage } = useLanguage();
|
||||
|
||||
const { data: outlet, isLoading: outletLoading, error: outletError, refetch: refetchOutlet } = useOutlet(id || '', language);
|
||||
const { data: articlesResponse, isLoading: articlesLoading, refetch: refetchArticles } = useArticlesByOutlet(id || '', language);
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await Promise.all([refetchOutlet(), refetchArticles()]);
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map(word => word[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
if (outletLoading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<Text style={styles.text}>Loading...</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (outletError || !outlet) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>Outlet not found</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Text style={styles.backButtonText}>Go Back</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const outletData = outlet as any;
|
||||
// Extract articles array from paginated response
|
||||
const articlesData = (articlesResponse as any)?.articles || [];
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
{outletData.avatar ? (
|
||||
<Image
|
||||
source={{ uri: outletData.avatar }}
|
||||
style={styles.outletAvatar}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.avatarFallback, styles.outletAvatar]}>
|
||||
<Text style={styles.avatarText}>{getInitials(outletData.name)}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.outletInfo}>
|
||||
<Image
|
||||
source={require('@/assets/images/sapiens-logo.png')}
|
||||
style={styles.miniLogo}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Text style={styles.outletName} numberOfLines={1}>{outletData.name}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.headerRight}>
|
||||
<TouchableOpacity style={styles.iconButton} onPress={() => setShowSearch(true)}>
|
||||
<Ionicons name="search-outline" size={18} color="#333" />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.iconButton} onPress={() => setShowLanguage(true)}>
|
||||
<Ionicons name="settings-outline" size={18} color="#333" />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.iconButton}>
|
||||
<Ionicons name="person-circle-outline" size={20} color="#333" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Content Area */}
|
||||
<View style={styles.contentContainer}>
|
||||
{showSearch ? (
|
||||
<SearchModal
|
||||
visible={showSearch}
|
||||
onClose={() => setShowSearch(false)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* Articles List */}
|
||||
<FlatList
|
||||
data={articlesData.filter((item: any) => item.newsId)}
|
||||
keyExtractor={(item: any) => item.newsId}
|
||||
renderItem={({ item, index }: any) => (
|
||||
<View style={styles.articleWrapper}>
|
||||
<ArticleCard
|
||||
key={item.newsId}
|
||||
id={item.newsId}
|
||||
title={item.title}
|
||||
summary={item.summary}
|
||||
thumbnail={item.thumbnail}
|
||||
outletName={outletData?.name || 'Unknown Outlet'}
|
||||
category={outletData?.category || 'uncategorized'}
|
||||
publishedAt={new Date(item.publishedAt)}
|
||||
timeAgo={item.timeAgo}
|
||||
onClick={(newsId) => {
|
||||
router.push(`/article/${newsId}`);
|
||||
}}
|
||||
isAd={item.isAd || false}
|
||||
tags={item.tags || []}
|
||||
index={index}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={
|
||||
articlesLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<Text style={styles.text}>Loading articles...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyText}>No articles available yet.</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Footer Navigation */}
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity style={styles.navButton} onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={24} color="#333" />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => router.push('/')}>
|
||||
<Image
|
||||
source={require('@/assets/images/sapiens-logo.png')}
|
||||
style={styles.footerLogo}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.navButton}>
|
||||
<Ionicons name="arrow-forward" size={24} color="#333" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Profile Modal */}
|
||||
<Modal
|
||||
visible={showProfileModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowProfileModal(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowProfileModal(false)}
|
||||
>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHeader}>
|
||||
<View style={styles.modalProfileContainer}>
|
||||
{outletData.avatar ? (
|
||||
<Image
|
||||
source={{ uri: outletData.avatar }}
|
||||
style={styles.modalAvatar}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.avatarFallback, styles.modalAvatar]}>
|
||||
<Text style={styles.avatarText}>{getInitials(outletData.name)}</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.modalName}>{outletData.name}</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => setShowProfileModal(false)}>
|
||||
<Text style={styles.closeButton}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.modalDescription}>
|
||||
{outletData.description || outletData.focusSubject}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
{/* Language Modal */}
|
||||
<LanguageModal
|
||||
visible={showLanguage}
|
||||
onClose={() => setShowLanguage(false)}
|
||||
currentLanguage={language}
|
||||
onSelectLanguage={setLanguage}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f5f5f5',
|
||||
},
|
||||
header: {
|
||||
backgroundColor: '#fff',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e0e0e0',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
headerLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
gap: 12,
|
||||
},
|
||||
outletAvatar: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
},
|
||||
outletInfo: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
gap: 3,
|
||||
flex: 1,
|
||||
},
|
||||
miniLogo: {
|
||||
height: 10,
|
||||
width: 90,
|
||||
resizeMode: 'contain',
|
||||
marginLeft: 0,
|
||||
},
|
||||
outletName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#000',
|
||||
maxWidth: 150,
|
||||
},
|
||||
categoryBadge: {
|
||||
backgroundColor: '#f0f0f0',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
},
|
||||
categoryText: {
|
||||
fontSize: 11,
|
||||
color: '#666',
|
||||
fontWeight: '500',
|
||||
},
|
||||
headerRight: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
profileContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
flex: 1,
|
||||
},
|
||||
avatar: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
},
|
||||
avatarFallback: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#e0e0e0',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatarText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
},
|
||||
profileInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
sapiensLogo: {
|
||||
height: 10,
|
||||
marginBottom: 2,
|
||||
},
|
||||
nameRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
name: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#000',
|
||||
},
|
||||
infoIcon: {
|
||||
fontSize: 16,
|
||||
color: '#007AFF',
|
||||
},
|
||||
iconButton: {
|
||||
padding: 8,
|
||||
},
|
||||
articleWrapper: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 16,
|
||||
},
|
||||
listContent: {
|
||||
paddingBottom: 16,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
emptyContainer: {
|
||||
padding: 32,
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
marginTop: 10,
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 16,
|
||||
color: 'red',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
textAlign: 'center',
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
width: '85%',
|
||||
maxWidth: 400,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 16,
|
||||
},
|
||||
modalProfileContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
flex: 1,
|
||||
},
|
||||
modalAvatar: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
},
|
||||
modalName: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#000',
|
||||
flex: 1,
|
||||
},
|
||||
closeButton: {
|
||||
fontSize: 24,
|
||||
color: '#666',
|
||||
fontWeight: '300',
|
||||
},
|
||||
modalDescription: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
lineHeight: 20,
|
||||
},
|
||||
footer: {
|
||||
backgroundColor: '#fff',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#e0e0e0',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
navButton: {
|
||||
padding: 8,
|
||||
},
|
||||
footerLogo: {
|
||||
height: 16,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user