diff --git a/.env.example b/.env.example index 3a10eb7..95380c6 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,2 @@ EXPO_PUBLIC_API_AUTHORIZATION_KEY="some key" -EXPO_PUBLIC_DEV_BASE_URL="https://dev.droidcon.co.ke/api/v1" -EXPO_PUBLIC_PROD_BASE_URL="https://api.droidcon.co.ke/v1" +EXPO_PUBLIC_BASE_URL="https://api.droidcon.co.ke/v1" diff --git a/app/(app)/[profile].tsx b/app/(app)/[profile].tsx new file mode 100644 index 0000000..ec75584 --- /dev/null +++ b/app/(app)/[profile].tsx @@ -0,0 +1,212 @@ +import { AntDesign, FontAwesome5 } from '@expo/vector-icons'; +import { useTheme } from '@react-navigation/native'; +import { Image } from 'expo-image'; +import * as Linking from 'expo-linking'; +import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; +import React, { useEffect, useState } from 'react'; +import { Dimensions, ImageBackground, Pressable, ScrollView, StyleSheet, View } from 'react-native'; +import Row from '../../components/common/Row'; +import Space from '../../components/common/Space'; +import StyledText from '../../components/common/StyledText'; +import MainContainer from '../../components/container/MainContainer'; +import { usePrefetchedEventData } from '../../services/api'; +import { getTwitterHandle } from '../../util/helpers'; + +type Profile = { + image?: string; + bio?: string; + twitter_handle?: string | null; + tagline?: string; + name?: string; +}; + +const { height } = Dimensions.get('window'); + +const Speaker = () => { + const { colors, dark } = useTheme(); + const router = useRouter(); + const { name, type } = useLocalSearchParams(); + const [details, setDetails] = useState({}); + + const { speakers, organizingTeam } = usePrefetchedEventData(); + + useEffect(() => { + if (type === 'speaker') { + const speaker = speakers?.data.filter((x) => x.name === name)[0]; + setDetails({ + image: speaker?.avatar, + bio: speaker?.biography, + twitter_handle: speaker?.twitter, + tagline: speaker?.tagline, + name: speaker?.name, + }); + } else if (type === 'organizer') { + const organizer = organizingTeam?.data.filter((x) => x.name === name)[0]; + setDetails({ + image: organizer?.photo, + bio: organizer?.bio, + twitter_handle: organizer?.twitter_handle, + tagline: organizer?.tagline, + name: organizer?.name, + }); + } + }, [name, organizingTeam?.data, speakers?.data, type]); + + const handleTwitterProfile = (link: string | null | undefined) => { + if (link) { + Linking.openURL(`${link}`); + } + }; + + return ( + + ( + router.back()} /> + ), + }} + /> + + + + + + + + + + {type === 'speaker' && } + + {type === 'speaker' ? 'Speaker' : 'Organizer'} + + + + {name} + + + + + + + {details?.tagline} + + + + + + + + + + Bio + + + + + + {details?.bio} + + + + + + + Twitter Handle + + handleTwitterProfile(details?.twitter_handle)} + > + + + + {details?.twitter_handle ? getTwitterHandle(details?.twitter_handle) : 'N/A'} + + + + + + ); +}; + +const styles = StyleSheet.create({ + main: { + flex: 1, + width: '100%', + }, + banner: { + height: height / 5, + width: '100%', + }, + centered: { + width: '100%', + alignItems: 'center', + }, + wrapper: { + width: '100%', + top: -40, + paddingHorizontal: 20, + }, + avatar: { + height: 100, + width: 100, + borderRadius: 60, + alignItems: 'center', + top: -50, + }, + text: { + textAlign: 'center', + }, + row: { + flexDirection: 'row', + alignItems: 'center', + gap: 5, + }, + image: { + height: 103, + width: 103, + borderWidth: 2, + borderRadius: 60, + }, + info: { + top: -40, + alignItems: 'center', + }, + contentText: { + lineHeight: 18, + }, + socialLink: { + width: '100%', + borderTopWidth: 1, + padding: 20, + marginBottom: 20, + }, + button: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-evenly', + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 8, + borderWidth: 1, + }, + taglineContainer: { + width: '70%', + }, +}); + +export default Speaker; diff --git a/app/(app)/[speaker].tsx b/app/(app)/[speaker].tsx deleted file mode 100644 index a105f52..0000000 --- a/app/(app)/[speaker].tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Stack, useLocalSearchParams } from 'expo-router'; -import React from 'react'; -import StyledText from '../../components/common/StyledText'; -import MainContainer from '../../components/container/MainContainer'; -// TODO: implement speaker page -/** - * - Should display information about the speaker or organizing team member - * - should display an image, speaker/organizer badge, name of speaker/organizer, bio and twitter handle at the bottom - */ -// TODO: Use data from mock/speaker to display speaker and mock/organizers.ts to display organizer - -const Speaker = () => { - const { name } = useLocalSearchParams(); - - return ( - - - speaker id {name} - - ); -}; - -export default Speaker; diff --git a/app/(app)/_layout.tsx b/app/(app)/_layout.tsx index 5e74011..66b2a1b 100644 --- a/app/(app)/_layout.tsx +++ b/app/(app)/_layout.tsx @@ -22,11 +22,12 @@ export default () => { }} /> diff --git a/app/(app)/home/about.tsx b/app/(app)/home/about.tsx index e3a6822..29e5054 100644 --- a/app/(app)/home/about.tsx +++ b/app/(app)/home/about.tsx @@ -1,10 +1,8 @@ -import { useTheme } from '@react-navigation/native'; import { FlashList } from '@shopify/flash-list'; -import { useQuery } from '@tanstack/react-query'; import { Image } from 'expo-image'; import { Stack, useRouter } from 'expo-router'; import React from 'react'; -import { ActivityIndicator, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import OrganizerCard from '../../../components/cards/OrganizerCard'; import OrganizersCard from '../../../components/cards/OrganizersCard'; import Space from '../../../components/common/Space'; @@ -13,18 +11,12 @@ import MainContainer from '../../../components/container/MainContainer'; import HeaderRight from '../../../components/headers/HeaderRight'; import { WIDE_BLURHASH } from '../../../config/constants'; import type { OrganizingTeamMember } from '../../../global'; -import { getOrganizingTeam, usePrefetchedEventData } from '../../../services/api'; +import { usePrefetchedEventData } from '../../../services/api'; const About = () => { const router = useRouter(); - const { colors } = useTheme(); - const { organizers } = usePrefetchedEventData(); - - const { isLoading, data: organizingTeam } = useQuery({ - queryKey: ['organizingTeam'], - queryFn: getOrganizingTeam, - }); + const { organizers, organizingTeam } = usePrefetchedEventData(); const organizingIndividuals = organizingTeam?.data.filter((item: OrganizingTeamMember) => item.type === 'individual'); @@ -81,26 +73,24 @@ const About = () => { - {isLoading ? ( - - ) : ( - - ( - router.push({ pathname: `/${item.name}`, params: { name: item.name } })} - /> - )} - keyExtractor={(item: OrganizingTeamMember, index: number) => index.toString()} - estimatedItemSize={50} - /> - - )} + + ( + + router.push({ pathname: `/${item.name}`, params: { name: item.name, type: 'organizer' } }) + } + /> + )} + keyExtractor={(item: OrganizingTeamMember, index: number) => index.toString()} + estimatedItemSize={50} + /> + {organizers && } diff --git a/app/(app)/home/feed/index.tsx b/app/(app)/home/feed/index.tsx index b2c447f..68d9d12 100644 --- a/app/(app)/home/feed/index.tsx +++ b/app/(app)/home/feed/index.tsx @@ -2,6 +2,7 @@ import { useTheme } from '@react-navigation/native'; import { useQuery } from '@tanstack/react-query'; import React from 'react'; import { ActivityIndicator, StyleSheet, View } from 'react-native'; +import StyledText from '../../../../components/common/StyledText'; import MainContainer from '../../../../components/container/MainContainer'; import FeedList from '../../../../components/lists/FeedList'; import { getEventFeed } from '../../../../services/api'; @@ -11,11 +12,21 @@ export default function Page() { const { isLoading, data } = useQuery({ queryKey: ['feed'], queryFn: getEventFeed }); + if (isLoading) { + return ( + + + + + + ); + } + return ( - {isLoading ? ( + {data?.data && data?.data.length < 1 ? ( - + This feed is empty for now. ) : ( {data && } @@ -26,6 +37,7 @@ export default function Page() { const styles = StyleSheet.create({ main: { + flex: 1, paddingHorizontal: 0, }, centered: { diff --git a/app/(app)/home/main.tsx b/app/(app)/home/main.tsx index f8e0636..8c1573b 100644 --- a/app/(app)/home/main.tsx +++ b/app/(app)/home/main.tsx @@ -44,11 +44,13 @@ const Main = () => { )} - - - {sponsors && } - - + {sponsors && sponsors.data.length > 1 && ( + <> + + + + + )} {organizers && } diff --git a/app/(app)/session/[session].tsx b/app/(app)/session/[session].tsx index 06d97f9..438826f 100644 --- a/app/(app)/session/[session].tsx +++ b/app/(app)/session/[session].tsx @@ -50,7 +50,7 @@ const Session = () => { }; return ( - + { ), }} /> - @@ -212,7 +211,6 @@ const Session = () => { - @@ -223,6 +221,9 @@ const Session = () => { export default Session; const styles = StyleSheet.create({ + page: { + flex: 1, + }, main: { flex: 1, width: '100%', diff --git a/assets/images/speaker-dark.png b/assets/images/speaker-dark.png new file mode 100644 index 0000000..43fe47b Binary files /dev/null and b/assets/images/speaker-dark.png differ diff --git a/assets/images/speaker-light.png b/assets/images/speaker-light.png new file mode 100644 index 0000000..9e79ea2 Binary files /dev/null and b/assets/images/speaker-light.png differ diff --git a/components/cards/SpeakerCard.tsx b/components/cards/SpeakerCard.tsx index 13187e3..74830bb 100644 --- a/components/cards/SpeakerCard.tsx +++ b/components/cards/SpeakerCard.tsx @@ -27,7 +27,7 @@ const SpeakerCard = (props: SpeakerCardProps) => { - + {name} diff --git a/components/lists/SpeakersList.tsx b/components/lists/SpeakersList.tsx index 140e015..8cbac6b 100644 --- a/components/lists/SpeakersList.tsx +++ b/components/lists/SpeakersList.tsx @@ -40,7 +40,9 @@ const SpeakersList = ({ speakers }: Props) => { router.push({ pathname: `/${item.name}`, params: { name: item.name } })} + handlePress={() => + router.push({ pathname: `/${item.name}`, params: { name: item.name, type: 'speaker' } }) + } /> )} keyExtractor={(item: Speaker, index: number) => index.toString()} diff --git a/config/constants.ts b/config/constants.ts index e3c5089..d8daf18 100644 --- a/config/constants.ts +++ b/config/constants.ts @@ -4,5 +4,5 @@ export const blurhash = export const VIDEO_URL = 'https://droidcon.co.ke/video/DroidconKe_2019_Highlight_Reel_HD.mp4'; export const WIDE_BLURHASH = 'LHF$LwM,IS%P.mIvsit9_1IXRiog'; -export const EVENT_SLUG = 'droidconke-2022-281'; +export const EVENT_SLUG = 'droidconke-2023-137'; export const ORG_SLUG = 'droidcon-ke-645'; diff --git a/config/theme.ts b/config/theme.ts index 3f4dc29..7102792 100644 --- a/config/theme.ts +++ b/config/theme.ts @@ -20,6 +20,7 @@ export const theme_colors: ThemeColors = { iconSwitch: '#20201E', placeHolder: '#C3C3C3', borderColor: '#F5F5F5', + borderColorSecondary: '#F5F5F5', assetAccent: '#000CEB', }, dark: { @@ -41,6 +42,7 @@ export const theme_colors: ThemeColors = { iconSwitch: '#20201E', placeHolder: '#C3C3C3', borderColor: '#707070', + borderColorSecondary: '#000000', assetAccent: '#000000', }, }; diff --git a/global.d.ts b/global.d.ts index f8b9f57..af431d1 100644 --- a/global.d.ts +++ b/global.d.ts @@ -27,6 +27,7 @@ declare module '@react-navigation/native' { placeHolder: string; borderColor: string; assetAccent: string; + borderColorSecondary: string; }; }; @@ -146,12 +147,12 @@ export interface Speaker { tagline: string; biography: string; avatar: string; - twitter: null | string; - facebook: null | string; - linkedin: null | string; - instagram: null | string; - blog: null | string; - company_website: null | string; + twitter?: string | null; + facebook?: string | null; + linkedin?: string | null; + instagram?: string | null; + blog?: string | null; + company_website?: string | null; } export interface ISpeaker { @@ -212,3 +213,5 @@ export type OrganizingTeamMember = { export interface IOrganizingTeam { data: Array; } + +export interface IProfile extends OrganizingTeamMember, Speaker {} diff --git a/package.json b/package.json index 54ec2c2..9a931c9 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "jest-expo": "49.0.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-native": "0.72.5", + "react-native": "0.72.6", "react-native-gesture-handler": "~2.12.0", "react-native-reanimated": "~3.3.0", "react-native-safe-area-context": "4.6.3", diff --git a/services/api/axios.ts b/services/api/axios.ts index fe08e15..3c3d248 100644 --- a/services/api/axios.ts +++ b/services/api/axios.ts @@ -1,7 +1,7 @@ import axios from 'axios'; export const axiosInstance = axios.create({ - baseURL: process.env.EXPO_PUBLIC_DEV_BASE_URL, + baseURL: process.env.EXPO_PUBLIC_BASE_URL, headers: { 'Api-Authorization-Key': process.env.EXPO_PUBLIC_API_AUTHORIZATION_KEY, Accept: 'application/json', diff --git a/services/api/index.ts b/services/api/index.ts index 1679fdd..5a412cf 100644 --- a/services/api/index.ts +++ b/services/api/index.ts @@ -73,8 +73,8 @@ const _queries = [ queryFn: () => getSessions(50), }, { - queryKey: ['speakers', 50], - queryFn: () => getSpeakers(50), + queryKey: ['speakers', 60], + queryFn: () => getSpeakers(60), }, { queryKey: ['sponsors'], @@ -88,12 +88,16 @@ const _queries = [ queryKey: ['schedule'], queryFn: () => getEventSchedule(), }, + { + queryKey: ['organizingTeam'], + queryFn: () => getOrganizingTeam(), + }, ] as const; export const prefetchEvent = async () => { _queries.forEach(async (query) => { await queryClient.prefetchQuery( - query as UseQueryOptions, + query as UseQueryOptions, ); }); }; @@ -113,6 +117,7 @@ export const usePrefetchedEventData = () => { const sponsors = queries[2]?.data; const organizers = queries[3]?.data; const schedule = queries[4]?.data; + const organizingTeam = queries[5]?.data; return { sessions, @@ -120,6 +125,7 @@ export const usePrefetchedEventData = () => { sponsors, organizers, schedule, + organizingTeam, isLoading: _isLoading, isRefetching: _isRefetching, refetch, diff --git a/util/helpers.ts b/util/helpers.ts index 1e6cb3a..df21ae4 100644 --- a/util/helpers.ts +++ b/util/helpers.ts @@ -94,8 +94,14 @@ export const getSessionTime = (startTime: string) => { * @example getTwitterHandle('https://twitter.com/kharioki') // returns kharioki */ export const getTwitterHandle = (url?: string) => { - const twitterHandle = url?.split('/').pop(); - return twitterHandle; + const regex = /twitter\.com\/([A-Za-z0-9_]+)/; + const match = url?.match(regex); + + if (match && match[1]) { + return match[1]; + } else { + return 'N/A'; + } }; /** diff --git a/yarn.lock b/yarn.lock index 13a0690..86397df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8155,10 +8155,10 @@ react-native-web@~0.19.6: postcss-value-parser "^4.2.0" styleq "^0.1.3" -react-native@0.72.5: - version "0.72.5" - resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.72.5.tgz#2c343fa6f3ead362cf07376634a33a4078864357" - integrity sha512-oIewslu5DBwOmo7x5rdzZlZXCqDIna0R4dUwVpfmVteORYLr4yaZo5wQnMeR+H7x54GaMhmgeqp0ZpULtulJFg== +react-native@0.72.6: + version "0.72.6" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.72.6.tgz#9f8d090694907e2f83af22e115cc0e4a3d5fa626" + integrity sha512-RafPY2gM7mcrFySS8TL8x+TIO3q7oAlHpzEmC7Im6pmXni6n1AuufGaVh0Narbr1daxstw7yW7T9BKW5dpVc2A== dependencies: "@jest/create-cache-key-function" "^29.2.1" "@react-native-community/cli" "11.3.7"