feat: 🎸 Skip to the next day in calendar (#425)

* Add meaningful starting date to components
* Add warn on console use
* Add better error message if response from skola24 is wrong
* Change heading to only show name of day
* Remove extra menu

Co-authored-by: Andreas Eriksson <addeman@gmail.com>
This commit is contained in:
Christian Landgren 2021-12-03 11:27:09 +01:00 committed by GitHub
parent 7bee08a550
commit fce1d98847
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 146 additions and 98 deletions

View File

@ -9,7 +9,7 @@
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
"rules": {"no-console":"warn"}
},
{
"files": ["*.ts", "*.tsx"],

View File

@ -16,7 +16,7 @@ import {
Text,
useStyleSheet,
} from '@ui-kitten/components'
import moment from 'moment'
import moment, { Moment } from 'moment'
import React, { useEffect } from 'react'
import { TouchableOpacity, useColorScheme, View } from 'react-native'
import { useTranslation } from '../hooks/useTranslation'
@ -31,6 +31,7 @@ interface ChildListItemProps {
child: Child
color: string
updated: string
currentDate?: Moment
}
type ChildListItemNavigationProp = StackNavigationProp<
RootStackParamList,
@ -41,6 +42,7 @@ export const ChildListItem = ({
child,
color,
updated,
currentDate = moment(),
}: ChildListItemProps) => {
// Forces rerender when child.id changes
React.useEffect(() => {
@ -57,16 +59,14 @@ export const ChildListItem = ({
const { data: menu, reload: menuReload } = useMenu(child)
const { data: schedule, reload: scheduleReload } = useSchedule(
child,
moment().toISOString(),
moment().add(7, 'days').toISOString()
moment(currentDate).toISOString(),
moment(currentDate).add(7, 'days').toISOString()
)
useEffect(() => {
// Do not refresh if updated is empty (first render of component)
if (updated === '') return
console.log('Reload', child.name, updated)
newsReload()
classmatesReload()
notificationsReload()
@ -89,8 +89,8 @@ export const ChildListItem = ({
)
const newsThisWeek = news.filter(({ modified, published }) => {
const date = modified || published
return date ? moment(date).isSame(moment(), 'week') : false
const newsDate = modified || published
return newsDate ? moment(newsDate).isSame(currentDate, 'week') : false
})
const scheduleAndCalendarThisWeek = [
@ -99,14 +99,18 @@ export const ChildListItem = ({
].filter(({ startDate }) =>
startDate
? moment(startDate).isBetween(
moment().startOf('day'),
moment().add(7, 'days')
moment(currentDate).startOf('day'),
moment(currentDate).add(7, 'days')
)
: false
)
const displayDate = (date: moment.MomentInput) => {
return moment(date).fromNow()
const displayDate = (inputDate: moment.MomentInput) => {
return moment(inputDate).fromNow()
}
const capitalizeFirstLetter = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1)
}
const getClassName = () => {
@ -168,12 +172,19 @@ export const ChildListItem = ({
/>
</View>
</View>
<DaySummary child={child} />
{currentDate.hour() > 17 && currentDate.hour() <= 23 ? (
<Text category="c2" style={styles.weekday}>
{capitalizeFirstLetter(currentDate.format('dddd'))}
</Text>
) : null}
<DaySummary child={child} date={currentDate} />
{scheduleAndCalendarThisWeek.slice(0, 3).map((calendarItem, i) => (
<Text category="p1" key={i}>
{`${calendarItem.title} (${displayDate(calendarItem.startDate)})`}
</Text>
))}
<Text category="c2" style={styles.label}>
{t('navigation.news')}
</Text>
@ -182,11 +193,13 @@ export const ChildListItem = ({
{notification.message}
</Text>
))}
{newsThisWeek.slice(0, 3).map((newsItem, i) => (
<Text category="p1" key={i}>
{newsItem.header ?? ''}
</Text>
))}
{scheduleAndCalendarThisWeek.length ||
notificationsThisWeek.length ||
newsThisWeek.length ? null : (
@ -194,14 +207,15 @@ export const ChildListItem = ({
{t('news.noNewNewsItemsThisWeek')}
</Text>
)}
{!menu[moment().isoWeekday() - 1] ? null : (
{!menu[currentDate.isoWeekday() - 1] ? null : (
<>
<Text category="c2" style={styles.label}>
{t('schedule.lunch')}
</Text>
<Text>{menu[moment().isoWeekday() - 1]?.description}</Text>
<Text>{menu[currentDate.isoWeekday() - 1]?.description}</Text>
</>
)}
<View style={styles.itemFooter}>
<Button
accessible
@ -274,4 +288,8 @@ const themeStyles = StyleService.create({
marginBottom: 0,
},
noNewNewsItemsText: {},
weekday: {
marginBottom: -5,
padding: 0,
},
})

View File

@ -26,6 +26,7 @@ import { Colors, Layout as LayoutStyle, Sizing, Typography } from '../styles'
import { translate } from '../utils/translation'
import { ChildListItem } from './childListItem.component'
import { SettingsIcon, RefreshIcon } from './icon.component'
import { getMeaningfulStartingDate } from '../utils/calendarHelpers'
const colors = ['primary', 'success', 'info', 'warning', 'danger']
@ -80,80 +81,77 @@ export const Children = () => {
})
}, [navigation, reloadChildren])
const currentDate = getMeaningfulStartingDate()
// We need to skip safe area view here, due to the reason that it's adding a white border
// when this view is actually lightgrey. Taking the padding top value from the use inset hook.
return (
<>
{status === 'loaded' ? (
<List
contentContainerStyle={styles.childListContainer}
data={childList}
style={styles.childList}
ListEmptyComponent={
<View style={styles.emptyState}>
<Text category="h2">{translate('children.noKids_title')}</Text>
<Text style={styles.emptyStateDescription}>
{translate('children.noKids_description')}
</Text>
<Image
accessibilityIgnoresInvertColors={false}
source={require('../assets/children.png')}
style={styles.emptyStateImage as ImageStyle}
/>
</View>
}
renderItem={({ item: child, index }: ListRenderItemInfo<Child>) => (
<ChildListItem
child={child}
color={colors[index % colors.length]}
key={child.id}
updated={updatedAt}
/>
)}
/>
) : (
<View style={styles.loading}>
return status === 'loaded' ? (
<List
contentContainerStyle={styles.childListContainer}
data={childList}
style={styles.childList}
ListEmptyComponent={
<View style={styles.emptyState}>
<Text category="h2">{translate('children.noKids_title')}</Text>
<Text style={styles.emptyStateDescription}>
{translate('children.noKids_description')}
</Text>
<Image
accessibilityIgnoresInvertColors={false}
source={require('../assets/girls.png')}
style={styles.loadingImage as ImageStyle}
source={require('../assets/children.png')}
style={styles.emptyStateImage as ImageStyle}
/>
{status === 'error' ? (
<View style={styles.errorMessage}>
<Text category="h5">
{translate('children.loadingErrorHeading')}
</Text>
<Text style={{ fontSize: Sizing.t4 }}>
{translate('children.loadingErrorInformationText')}
</Text>
<View style={styles.errorButtons}>
<Button status="success" onPress={() => reloadChildren()}>
{translate('children.tryAgain')}
</Button>
<Button
status="basic"
onPress={() =>
Linking.openURL('https://skolplattformen.org/status')
}
>
{translate('children.viewStatus')}
</Button>
<Button onPress={() => logout()}>
{translate('general.logout')}
</Button>
</View>
</View>
) : (
<View style={styles.loadingMessage}>
<Spinner size="large" status="primary" />
<Text category="h1" style={styles.loadingText}>
{translate('general.loading')}
</Text>
</View>
)}
</View>
}
renderItem={({ item: child, index }: ListRenderItemInfo<Child>) => (
<ChildListItem
child={child}
color={colors[index % colors.length]}
currentDate={currentDate}
updated={updatedAt}
key={child.id}
/>
)}
/>
) : (
<View style={styles.loading}>
<Image
accessibilityIgnoresInvertColors={false}
source={require('../assets/girls.png')}
style={styles.loadingImage as ImageStyle}
/>
{status === 'error' ? (
<View style={styles.errorMessage}>
<Text category="h5">{translate('children.loadingErrorHeading')}</Text>
<Text style={{ fontSize: Sizing.t4 }}>
{translate('children.loadingErrorInformationText')}
</Text>
<View style={styles.errorButtons}>
<Button status="success" onPress={() => reloadChildren()}>
{translate('children.tryAgain')}
</Button>
<Button
status="basic"
onPress={() =>
Linking.openURL('https://skolplattformen.org/status')
}
>
{translate('children.viewStatus')}
</Button>
<Button onPress={() => logout()}>
{translate('general.logout')}
</Button>
</View>
</View>
) : (
<View style={styles.loadingMessage}>
<Spinner size="large" status="primary" />
<Text category="h1" style={styles.loadingText}>
{translate('general.loading')}
</Text>
</View>
)}
</>
</View>
)
}

View File

@ -2,7 +2,7 @@ import { Child } from '@skolplattformen/api'
import { useTimetable } from '@skolplattformen/hooks'
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
import moment, { Moment } from 'moment'
import React from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { View } from 'react-native'
import { LanguageService } from '../services/languageService'
import { translate } from '../utils/translation'
@ -12,9 +12,13 @@ interface DaySummaryProps {
date?: Moment
}
export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => {
export const DaySummary = ({
child,
date: currentDate = moment(),
}: DaySummaryProps) => {
const styles = useStyleSheet(themedStyles)
const [year, week] = [moment().isoWeekYear(), moment().isoWeek()]
const [week, year] = [currentDate.isoWeek(), currentDate.isoWeekYear()]
const { data: weekLessons } = useTimetable(
child,
week,
@ -23,7 +27,7 @@ export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => {
)
const lessons = weekLessons
.filter((lesson) => lesson.dayOfWeek === date.isoWeekday())
.filter((lesson) => lesson.dayOfWeek === currentDate.isoWeekday())
.sort((a, b) => a.dateStart.localeCompare(b.dateStart))
if (lessons.length <= 0) {
@ -53,15 +57,21 @@ export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => {
</Text>
</View>
</View>
<View style={styles.part}>
<View>
<Text category="c2" style={styles.label}>
&nbsp;
</Text>
<Text category="s2">
{gymBag
? ` 🤼‍♀️ ${translate('schedule.gymBag', {
defaultValue: 'Gympapåse',
})}`
: ''}
</Text>
</View>
</View>
</View>
<Text category="s2">
{gymBag
? ` 🤼‍♀️ ${translate('schedule.gymBag', {
defaultValue: 'Gympapåse',
})}`
: ''}
</Text>
</View>
)
}
@ -76,4 +86,7 @@ const themedStyles = StyleService.create({
label: {
marginTop: 10,
},
heading: {
marginBottom: -10,
},
})

View File

@ -16,6 +16,7 @@ import { View } from 'react-native'
import { LanguageService } from '../services/languageService'
import { Sizing, Typography } from '../styles'
import { TransitionView } from './transitionView.component'
import { getMeaningfulStartingDate } from '../utils/calendarHelpers'
interface WeekProps {
child: Child
@ -107,10 +108,12 @@ export const Day = ({ weekDay, lunch, lessons }: DayProps) => {
export const Week = ({ child }: WeekProps) => {
moment.locale(LanguageService.getLocale())
const days = moment.weekdaysShort().slice(1, 6)
const currentDayIndex = Math.min(moment().isoWeekday() - 1, 5)
const displayDate = getMeaningfulStartingDate()
const currentDayIndex = Math.min(moment(displayDate).isoWeekday() - 1, 5)
const [selectedIndex, setSelectedIndex] = useState(currentDayIndex)
const [showSchema, setShowSchema] = useState(false)
const [year, week] = [moment().isoWeekYear(), moment().isoWeek()]
const [year, week] = [displayDate.isoWeekYear(), displayDate.isoWeek()]
const { data: lessons } = useTimetable(
child,
week,

View File

@ -80,7 +80,8 @@
"send": "Send",
"settings": "Settings",
"socialSecurityNumber": "Personal identity number",
"title": "Öppna skolplattformen"
"title": "Öppna skolplattformen",
"tomorrow": "Tomorrow"
},
"language": {
"changeLanguage": "Change language",

View File

@ -82,7 +82,8 @@
"title": "Öppna skolplattformen",
"cancel": "Avbryt",
"logoutAndClearAllDataInclSettings": "Logga ut och rensa all sparad data inkl inställningar",
"logoutAndClearPersonalData": "Logga ut och rensa all personlig data"
"logoutAndClearPersonalData": "Logga ut och rensa all personlig data",
"tomorrow": "Imorgon"
},
"language": {
"changeLanguage": "Byt språk",

View File

@ -0,0 +1,9 @@
import moment from 'moment'
export const getMeaningfulStartingDate = (date = moment()) => {
// are we on the evening?
if (date.hour() > 17) date = date.add('1', 'day')
// are we on the weekend
if (date.isoWeekday() > 5) date = date.add(5, 'days').startOf('isoWeek')
return date
}

View File

@ -84,6 +84,11 @@ export const timetable = (
if (response.error) {
throw new Error(response.error)
}
if(!response.data.lessonInfo){
throw new Error("Empty lessonInfo received")
}
return response.data.lessonInfo.map((entry) =>
timetableEntry(entry, year, week, lang)
)