Skip to content

Commit

Permalink
feat(lecturers): adds card and page
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert27 committed Aug 20, 2023
1 parent 25750dd commit 21106fd
Show file tree
Hide file tree
Showing 11 changed files with 558 additions and 12 deletions.
4 changes: 2 additions & 2 deletions src/api/authenticated-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
return res
}

async getPersonalLecturers(): Promise<Lecturers> {
async getPersonalLecturers(): Promise<Lecturers[]> {
const res = await this.requestCached(KEY_GET_PERSONAL_LECTURERS, {
service: 'thiapp',
method: 'stpllecturers',
Expand All @@ -238,7 +238,7 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
* @param {string} from Single character indicating where to start listing the lecturers
* @param {string} to Single character indicating where to end listing the lecturers
*/
async getLecturers(from: string, to: string): Promise<Lecturers> {
async getLecturers(from: string, to: string): Promise<Lecturers[]> {
const key = `${KEY_GET_LECTURERS}-${from}-${to}`
const res = await this.requestCached(key, {
service: 'thiapp',
Expand Down
145 changes: 145 additions & 0 deletions src/app/(pages)/lecturer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import FormList from '@/components/Elements/Universal/FormList'
import { type Colors } from '@/stores/colors'
import { type FormListSections } from '@/stores/types/components'
import { type NormalizedLecturer } from '@/utils/lecturers-utils'
import { useTheme } from '@react-navigation/native'
import { router, useLocalSearchParams } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
import React from 'react'
import { Linking, ScrollView, StyleSheet, Text, View } from 'react-native'

export default function LecturerDetail(): JSX.Element {
const colors = useTheme().colors as Colors
const { lecturerEntry } = useLocalSearchParams<{ lecturerEntry: string }>()
const lecturer: NormalizedLecturer | undefined =
lecturerEntry != null ? JSON.parse(lecturerEntry) : undefined

const sections: FormListSections[] = [
{
header: 'Details',
items: [
{
title: 'Name',
value: `${lecturer?.vorname ?? ''} ${lecturer?.name ?? ''}`,
disabled: true,
},
{
title: 'Title',
value: lecturer?.titel,
disabled: true,
},
{
title: 'Organization',
value: lecturer?.organisation,
disabled: true,
},

{
title: 'Function',
value: lecturer?.funktion,
disabled: true,
},
],
},
{
header: 'Contact',
items: [
{
title: 'Room',
value: lecturer?.room_short,
disabled: lecturer?.room_short == null,
iconColor: colors.primary,
onPress: () => {
router.push('(tabs)/map')
router.setParams({
q: lecturer?.room_short ?? '',
h: 'true',
})
},
},
{
title: 'E-Mail',
value: lecturer?.email,
disabled: lecturer?.email == null,
iconColor: colors.primary,
onPress: () => {
void Linking.openURL(`mailto:${lecturer?.email ?? ''}`)
},
},
{
title: 'Phone',
value: lecturer?.tel_dienst,
disabled: lecturer?.tel_dienst == null,
iconColor: colors.primary,
onPress: () => {
void Linking.openURL(
`tel:${
lecturer?.tel_dienst?.replace(/\s+/g, '') ?? ''
}`
)
},
},
{
title: 'Office Hours',
value: lecturer?.sprechstunde,
disabled: true,
},
{
title: 'Exam Insigths',
value: lecturer?.einsichtnahme,
disabled: true,
},
],
},
]

return (
<ScrollView>
<StatusBar style="light" animated={true} hidden={false} />
<View
style={[
styles.titleContainer,
{ backgroundColor: colors.card },
]}
>
<Text
style={[styles.titleText, { color: colors.text }]}
allowFontScaling={true}
adjustsFontSizeToFit={true}
numberOfLines={2}
>
{`${[lecturer?.titel, lecturer?.vorname, lecturer?.name]
.join(' ')
.trim()}`}
</Text>
</View>
<FormList sections={sections} />
</ScrollView>
)
}

const styles = StyleSheet.create({
titleContainer: {
alignSelf: 'center',
width: '92%',
marginTop: 20,
paddingHorizontal: 5,
paddingVertical: 10,
borderRadius: 8,
alignItems: 'center',
},
titleText: {
fontSize: 18,
textAlign: 'center',
},
notesContainer: {
alignSelf: 'center',
width: '92%',
marginTop: 20,
marginBottom: 40,
},
notesText: {
textAlign: 'justify',
fontSize: 13,
},
})
226 changes: 226 additions & 0 deletions src/app/(pages)/lecturers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import API from '@/api/authenticated-api'
import {
NoSessionError,
UnavailableSessionError,
} from '@/api/thi-session-handler'
import LecturerRow from '@/components/Elements/Pages/LecturerRow'
import Divider from '@/components/Elements/Universal/Divider'
import { type Colors } from '@/stores/colors'
import {
type NormalizedLecturer,
normalizeLecturers,
} from '@/utils/lecturers-utils'
import { useTheme } from '@react-navigation/native'
import { useGlobalSearchParams, useRouter } from 'expo-router'
import React, { useEffect, useState } from 'react'
import {
ActivityIndicator,
RefreshControl,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native'

export default function LecturersCard(): JSX.Element {
enum LoadingState {
LOADING,
LOADED,
ERROR,
REFRESHING,
}

const [personalLecturers, setPersonalLecturers] = useState<
NormalizedLecturer[]
>([])
const [filteredLecturers, setFilteredLecturers] = useState<
NormalizedLecturer[]
>([])
const [didFetch, setDidFetch] = useState(false)
const [error, setError] = useState<Error | null>(null)
const [loadingState, setLoadingState] = useState<LoadingState>(
LoadingState.LOADING
)
const { q } = useGlobalSearchParams<{ q: string }>()
const [allLecturers, setAllLecturers] = useState<NormalizedLecturer[]>([])
const colors = useTheme().colors as Colors
const router = useRouter()
async function load(): Promise<void> {
try {
const rawData = await API.getPersonalLecturers()
const data = normalizeLecturers(rawData)
setPersonalLecturers(data)
setLoadingState(LoadingState.LOADED)
} catch (e) {
if (
e instanceof NoSessionError ||
e instanceof UnavailableSessionError
) {
router.replace('(user)/login')
} else {
setLoadingState(LoadingState.ERROR)
setError(e as Error)
}
}
}
useEffect(() => {
void load()
}, [])

const onRefresh: () => void = () => {
void load()
setLoadingState(LoadingState.LOADED)
}

useEffect(() => {
async function load(): Promise<void> {
setLoadingState(LoadingState.LOADING)
if (q == null) {
setFilteredLecturers(personalLecturers)
setLoadingState(LoadingState.LOADED)
return
}

if (allLecturers.length === 0) {
if (didFetch) {
return
}

setDidFetch(true)
setFilteredLecturers([])
try {
const rawData = await API.getLecturers('0', 'z')
const data = normalizeLecturers(rawData)
setAllLecturers(data)
setLoadingState(LoadingState.LOADED)
return
} catch (e) {
if (e instanceof NoSessionError) {
router.replace('(user)/login')
} else {
setError(e as Error)
}
setLoadingState(LoadingState.ERROR)
return
}
}

const normalizedSearch = q.toLowerCase().trim()
const checkField = (value: string | null): boolean =>
value?.toString().toLowerCase().includes(normalizedSearch) ??
false
const filtered = allLecturers
.filter(
(x) =>
checkField(x.name) ||
checkField(x.vorname) ||
checkField(x.email) ||
checkField(x.tel_dienst) ||
checkField(x.raum)
)
.slice(0, 20)

setFilteredLecturers(filtered)
setLoadingState(LoadingState.LOADED)
}
void load()
}, [didFetch, q, personalLecturers, allLecturers])

return (
<ScrollView
contentInsetAdjustmentBehavior="automatic"
refreshControl={
loadingState !== LoadingState.LOADING &&
loadingState !== LoadingState.LOADED ? (
<RefreshControl
refreshing={loadingState === LoadingState.REFRESHING}
onRefresh={onRefresh}
/>
) : undefined
}
>
{loadingState === LoadingState.LOADING && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color={colors.primary} />
</View>
)}
{loadingState === LoadingState.ERROR && (
<View>
<Text style={[styles.errorMessage, { color: colors.text }]}>
{error?.message}
</Text>
<Text style={[styles.errorInfo, { color: colors.text }]}>
An error occurred while loading the data.{'\n'}Pull down
to refresh.
</Text>
</View>
)}

{loadingState === LoadingState.LOADED && (
<View style={styles.loadedContainer}>
<Text
style={[
styles.sectionHeader,
{ color: colors.labelSecondaryColor },
]}
>
{q != null ? 'Suchergebnisse' : 'Persönliche Dozenten'}
</Text>
<View
style={[
styles.loadedRows,
{ backgroundColor: colors.card },
]}
>
{filteredLecturers?.map((event, index) => (
<React.Fragment key={index}>
<LecturerRow item={event} colors={colors} />
{index !== personalLecturers.length - 1 && (
<Divider
color={colors.labelTertiaryColor}
width={'90%'}
/>
)}
</React.Fragment>
))}
</View>
</View>
)}
</ScrollView>
)
}

const styles = StyleSheet.create({
loadedContainer: {
alignSelf: 'center',
width: '95%',
marginTop: 14,
marginBottom: 24,
},
loadedRows: {
borderRadius: 8,
},
errorMessage: {
paddingTop: 100,
fontWeight: '600',
fontSize: 16,
textAlign: 'center',
},
errorInfo: {
fontSize: 14,
textAlign: 'center',
marginTop: 10,
},
loadingContainer: {
paddingTop: 40,
justifyContent: 'center',
alignItems: 'center',
},
sectionHeader: {
fontSize: 13,

fontWeight: 'normal',
textTransform: 'uppercase',
marginBottom: 4,
},
})
2 changes: 1 addition & 1 deletion src/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function HomeLayout(): JSX.Element {
tabBarLabelStyle: {
marginBottom: 2,
},
lazy: true,
lazy: false,
}}
>
<Tabs.Screen
Expand Down
Loading

0 comments on commit 21106fd

Please sign in to comment.