diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 7fbd1ff..25fb9ad 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -10,6 +10,12 @@ export default function TabLayout() { headerShown: false, }} /> + ); } diff --git a/app/(tabs)/home/_layout.tsx b/app/(tabs)/home/_layout.tsx index ffc3993..58ae6dd 100644 --- a/app/(tabs)/home/_layout.tsx +++ b/app/(tabs)/home/_layout.tsx @@ -81,7 +81,7 @@ export default function TabLayout() { ), headerStyle: { backgroundColor: Colors.palette.primary, - height: 120, + height: 110, elevation: 0, shadowOpacity: 0, borderBottomWidth: 0, diff --git a/app/(tabs)/home/speakers.tsx b/app/(tabs)/home/speakers.tsx index f07ca9a..a5a914a 100644 --- a/app/(tabs)/home/speakers.tsx +++ b/app/(tabs)/home/speakers.tsx @@ -1,49 +1,36 @@ -import React, { useEffect, useState } from 'react'; -import { FlatList, View, ActivityIndicator, StyleSheet, Text } from 'react-native'; -import palette from '@/constants/Colors'; // Use your color palette +import React from 'react'; +import { FlatList, View, ActivityIndicator, StyleSheet } from 'react-native'; +import palette from '@/constants/Colors'; import SpeakerCard from '@/components/SpeakerCard'; -import { Speaker } from '@/components/types'; +import BackgroundWrapper from '@/components/containers/BackgroundWrapper'; +import { useFetchSpeakers } from '@/hooks/useFetchSpeakers'; +import StyledText from '@/components/common/StyledText'; // Import StyledText +import { Stack } from 'expo-router'; const SpeakersTab = () => { - const [speakerList, setSpeakerList] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - - const fetchSpeakers = async () => { - setLoading(true); - setError(false); - try { - const res = await fetch('https://sessionize.com/api/v2/d899srzm/view/Speakers'); - if (!res.ok) throw new Error('Failed to fetch speakers'); - const data = await res.json(); - setSpeakerList(data); - } catch (error) { - setError(true); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchSpeakers(); - }, []); + const { speakerList, loading, error } = useFetchSpeakers(); return ( - - Speakers - - {loading ? ( - - ) : error ? ( - Failed to load speakers. Please try again later. - ) : ( - } - keyExtractor={(item) => item.id} - /> - )} - + + + + Speakers + + {loading ? ( + + ) : error ? ( + + Failed to load speakers. Please try again later. + + ) : ( + } + keyExtractor={(item) => item.id} + /> + )} + + ); }; @@ -51,18 +38,13 @@ const styles = StyleSheet.create({ container: { flex: 1, padding: 16, - backgroundColor: palette.palette.primary, }, header: { - fontSize: 24, - fontWeight: '700', - color: palette.palette.secondary, - textAlign: 'center', - marginBottom: 16, + marginBottom: 16, // Adjusted to ensure space between the header and the list }, error: { textAlign: 'center', - color: palette.palette.error, + marginTop: 16, // Added margin to position error message properly }, }); diff --git a/app/(tabs)/speakers/[id].tsx b/app/(tabs)/speakers/[id].tsx index bd69ddc..566a8fc 100644 --- a/app/(tabs)/speakers/[id].tsx +++ b/app/(tabs)/speakers/[id].tsx @@ -1,107 +1,103 @@ -import React, { useEffect, useState } from 'react'; -import { View, Text, Image, StyleSheet, ActivityIndicator, ScrollView, Linking } from 'react-native'; -import { useLocalSearchParams } from 'expo-router'; +import React, { useState, useMemo } from 'react'; +import { View, ScrollView, ActivityIndicator, StyleSheet, TouchableOpacity, Linking, Image } from 'react-native'; +import { Stack, useLocalSearchParams, useNavigation } from 'expo-router'; import palette from '@/constants/Colors'; -import { FontAwesome } from '@expo/vector-icons'; // Importing FontAwesome icons for Twitter (X) and LinkedIn - -type Speaker = { - id: string; - fullName: string; - profilePicture: string; - occupation: string; - bio: string; - socialMedia?: { - twitter?: string; - linkedin?: string; - }; - sessions: { - time: string; - title: string; - }[]; -}; +import { useFetchSpeakers } from '@/hooks/useFetchSpeakers'; +import SpeakerHeader from '@/components/headers/SpeakerHeader'; +import BackgroundWrapper from '@/components/containers/BackgroundWrapper'; +import StyledText from '@/components/common/StyledText'; +import { FontAwesome } from '@expo/vector-icons'; const SpeakerPage = () => { const { id } = useLocalSearchParams(); // Fetch dynamic ID from route - const [speaker, setSpeaker] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - // Simulate fetching the speaker data based on the dynamic ID - const fetchSpeaker = async () => { - try { - const res = await fetch(`https://sessionize.com/api/v2/d899srzm/view/Speakers`); - const data = await res.json(); - const speakerData = data.find((s: Speaker) => s.id === id); // Find speaker by dynamic ID - setSpeaker(speakerData); - } catch (error) { - console.error(error); - } finally { - setLoading(false); - } - }; + const navigation = useNavigation(); + const { speakerList, loading, error } = useFetchSpeakers(); + const [savedSessions, setSavedSessions] = useState<{ [key: string]: boolean }>({}); - fetchSpeaker(); - }, [id]); + const speaker = useMemo(() => speakerList.find((s) => s.id === id), [speakerList, id]); if (loading) { return ; } - if (!speaker) { - return Speaker not found; + if (error || !speaker) { + return Speaker not found; } - // Fallbacks for social media links - const twitterLink = speaker.socialMedia?.twitter || 'https://twitter.com'; // Fallback Twitter link - const linkedinLink = speaker.socialMedia?.linkedin || 'https://linkedin.com'; // Fallback LinkedIn link - - return ( - - - {speaker.fullName} - - {speaker.occupation} - + const toggleSaveSession = (sessionIndex: number) => { + setSavedSessions((prevState) => ({ + ...prevState, + [sessionIndex]: !prevState[sessionIndex], + })); + }; - - openURL(twitterLink)} - style={styles.socialIcon} - /> - openURL(linkedinLink)} - style={styles.socialIcon} - /> - + const socialMediaLinks = [ + { name: 'twitter', url: speaker.socialMedia?.twitter || 'https://x.com/renderconke', icon: require('@/assets/images/x.png') }, + { name: 'linkedin', url: speaker.socialMedia?.linkedin || 'https://www.linkedin.com/company/renderconke/', icon: require('@/assets/images/linkedin.png') + }, + ]; - {speaker.bio} + return ( + + + {/* Back Button */} + navigation.goBack()}> + + - {speaker.sessions.map((session, index) => ( - - {session.time} - {session.title} + + + + {speaker.bio} + + + + {socialMediaLinks.map((social, index) => ( + Linking.openURL(social.url)} style={styles.socialIconImage}> + + + ))} - ))} - - ); -}; -const openURL = (url: string) => { - // Function to open social media links - Linking.openURL(url); + {speaker.sessions.map((session, index) => ( + + + + + {session.time} + + + {session.name} + + + toggleSaveSession(index)}> + + + + + ))} + + + ); }; const styles = StyleSheet.create({ container: { - flex: 1, - backgroundColor: palette.palette.primary, padding: 16, + justifyContent: 'center', + marginTop: 30, + + }, + backButton: { + position: 'absolute', + top: 10, + left: 10, + zIndex: 10, + padding: 8, }, loader: { flex: 1, @@ -109,61 +105,49 @@ const styles = StyleSheet.create({ alignItems: 'center', }, error: { - color: palette.palette.error, textAlign: 'center', marginTop: 20, }, - header: { - alignItems: 'center', + bio: { + marginTop: 0, marginBottom: 20, - }, - name: { - fontSize: 24, - fontWeight: 'bold', - color: palette.palette.secondary, - }, - image: { - width: 100, - height: 100, - borderRadius: 50, - marginVertical: 16, - }, - occupation: { - fontSize: 18, - color: palette.palette.secondary, - marginBottom: 12, + lineHeight: 22, }, socialMediaContainer: { flexDirection: 'row', justifyContent: 'center', - marginVertical: 12, - }, - socialIcon: { - marginHorizontal: 15, + marginVertical: 15, + gap: 16, }, - bio: { - fontSize: 14, - color: palette.palette.text, - marginVertical: 20, - lineHeight: 22, + socialIconImage: { + borderWidth: 1, + borderColor: '#eee', + padding: 6, + borderRadius: 5, + backgroundColor: 'rgba(250, 250, 250, 0.2)', }, sessionCard: { - backgroundColor: '#2c2c54', + backgroundColor: 'rgba(250, 250, 250, 0.2)', padding: 16, borderRadius: 12, marginBottom: 16, borderWidth: 1, borderColor: '#eee', }, + sessionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 4, + }, sessionTime: { - fontSize: 16, color: '#eee', }, sessionTitle: { - fontSize: 18, - fontWeight: 'bold', - color: '#fff', marginTop: 4, + marginRight: 6, + flexShrink: 1, + flexWrap: 'wrap', }, }); diff --git a/app/(tabs)/speakers/_layout.tsx b/app/(tabs)/speakers/_layout.tsx index 341f5b9..ab62cb7 100644 --- a/app/(tabs)/speakers/_layout.tsx +++ b/app/(tabs)/speakers/_layout.tsx @@ -3,7 +3,7 @@ import { Stack } from 'expo-router'; export default function TabLayout() { return ( - + = ({ speaker }) => { ) : ( - No Image + No Image )} - {speaker.fullName} + + {speaker.fullName} + - {speaker.sessions[0]?.name || 'Session you cant afford to miss!'} + + {speaker.sessions[0]?.name || 'Session you can’t afford to miss!'} + @@ -29,8 +34,8 @@ const SpeakerCard: React.FC = ({ speaker }) => { const styles = StyleSheet.create({ card: { - backgroundColor: '#351e4a', // primary color - borderRadius: 12, + backgroundColor: 'rgba(250, 250, 250,0.15)', + borderRadius: 5, flexDirection: 'row', alignItems: 'center', padding: 8, @@ -39,6 +44,8 @@ const styles = StyleSheet.create({ shadowOpacity: 0.25, shadowRadius: 6, shadowOffset: { width: 0, height: 2 }, + borderWidth: 1, + borderColor: '#eee', }, image: { width: 80, @@ -58,19 +65,15 @@ const styles = StyleSheet.create({ }, imageFallbackText: { color: '#fff', - fontSize: 12, }, textContainer: { flex: 1, }, name: { - fontSize: 18, - fontWeight: '700', - color: '#eee712', // secondary color + marginBottom: 4, // Adjusted for spacing between name and occupation }, occupation: { - fontSize: 14, - color: '#fff', // text color + color: '#fff', // You can customize the color here if needed marginTop: 4, }, }); diff --git a/components/Speakers.tsx b/components/Speakers.tsx index 63d5708..27b6da8 100644 --- a/components/Speakers.tsx +++ b/components/Speakers.tsx @@ -1,43 +1,25 @@ -import React, { useEffect, useState } from 'react'; -import { FlatList, View, ActivityIndicator, StyleSheet, Text } from 'react-native'; -import SpeakerCard from './SpeakerCard'; // Assuming it's in the same directory +import React from 'react'; +import { FlatList, View, ActivityIndicator, StyleSheet } from 'react-native'; +import SpeakerCard from './SpeakerCard'; import palette from '@/constants/Colors'; // Use default import if Colors is exported as default -import { Speaker } from './types'; // Import the Speaker type +import { useFetchSpeakers } from '@/hooks/useFetchSpeakers'; // Use the custom hook import StyledText from './common/StyledText'; const Speakers = () => { - const [speakerList, setSpeakerList] = useState([]); // Use Speaker[] type for the array - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - - const fetchSpeakers = async () => { - setLoading(true); - setError(false); - try { - const res = await fetch('https://sessionize.com/api/v2/d899srzm/view/Speakers'); - if (!res.ok) throw new Error('Failed to fetch speakers'); - const data = await res.json(); - setSpeakerList(data); - } catch (error) { - setError(true); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchSpeakers(); - }, []); + const { speakerList, loading, error } = useFetchSpeakers(); // Use the custom hook to fetch speakers return ( + {/* StyledText replaces Text for a consistent font style */} Speakers {loading ? ( - // Access secondary color correctly + ) : error ? ( - Failed to load speakers. Please try again later. + + Failed to load speakers. Please try again later. + ) : ( ; +} + +const BackgroundWrapper: React.FC = ({ children, style }) => { + return ( + + {children} + + ); +}; + +const styles = StyleSheet.create({ + background: { + flex: 1, + width: '100%', + height: '100%', + backgroundColor: palette.palette.primary, + }, +}); + +export default BackgroundWrapper; diff --git a/components/headers/SpeakerHeader.tsx b/components/headers/SpeakerHeader.tsx new file mode 100644 index 0000000..f8b6e7f --- /dev/null +++ b/components/headers/SpeakerHeader.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { View, Image, StyleSheet } from 'react-native'; +import palette from '@/constants/Colors'; +import StyledText from '@/components/common/StyledText'; // Import the StyledText component + +interface SpeakerHeaderProps { + name: string; + occupation: string; + profilePicture: string; +} + +const SpeakerHeader: React.FC = ({ name, occupation, profilePicture }) => { + return ( + + {/* Replaced Text with StyledText */} + + {name} + + + + {occupation} + + + ); +}; + +const styles = StyleSheet.create({ + header: { + alignItems: 'center', + marginBottom: 20, + }, + name: { + marginBottom: 8, + color: palette.palette.secondary + }, + image: { + width: 150, + height: 150, + borderRadius: 20, + marginVertical: 16, + }, + occupation: { + marginBottom: 12, + }, +}); + +export default SpeakerHeader; diff --git a/components/types.ts b/components/types.ts index 757176f..e5bca63 100644 --- a/components/types.ts +++ b/components/types.ts @@ -1,7 +1,15 @@ -// types.ts export type Speaker = { id: string; fullName: string; profilePicture: string; - sessions: { name: string }[]; + occupation: string; // Add occupation property + bio: string; // Add bio property (since it is used in the speaker page) + socialMedia?: { + twitter?: string; + linkedin?: string; + }; // Add optional socialMedia object with twitter and linkedin fields + sessions: { + name: string; // Add time property to each session + time: string; // Add title property to each session + }[]; }; diff --git a/hooks/useFetchSpeakers.tsx b/hooks/useFetchSpeakers.tsx new file mode 100644 index 0000000..2cb8b84 --- /dev/null +++ b/hooks/useFetchSpeakers.tsx @@ -0,0 +1,29 @@ +import { useState, useEffect } from 'react'; +import { Speaker } from '@/components/types'; + +export const useFetchSpeakers = () => { + const [speakerList, setSpeakerList] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + const fetchSpeakers = async () => { + setLoading(true); + setError(false); + try { + const res = await fetch('https://sessionize.com/api/v2/d899srzm/view/Speakers'); + if (!res.ok) throw new Error('Failed to fetch speakers'); + const data = await res.json(); + setSpeakerList(data); + } catch (error) { + setError(true); + } finally { + setLoading(false); + } + }; + + fetchSpeakers(); + }, []); + + return { speakerList, loading, error }; +};