refactor(app): move to typescript

This commit is contained in:
Rickard Natt och Dag 2021-03-26 09:38:15 +01:00
parent 081497ec97
commit 203a0a302b
No known key found for this signature in database
GPG Key ID: C3958EFC7F24E8DF
26 changed files with 584 additions and 224 deletions

View File

@ -1 +1 @@
export default from '@react-native-async-storage/async-storage/jest/async-storage-mock'
export { default } from '@react-native-async-storage/async-storage/jest/async-storage-mock'

View File

@ -1,3 +1,4 @@
import { NavigationProp } from '@react-navigation/native'
import { Layout, Text } from '@ui-kitten/components'
import React, { useState } from 'react'
import {
@ -10,6 +11,11 @@ import {
View,
} from 'react-native'
import { Login } from './login.component'
import { SigninStackParamList } from './navigation.component'
interface AuthProps {
navigation: NavigationProp<SigninStackParamList, 'Login'>
}
const funArguments = [
'agila',
@ -31,7 +37,7 @@ const funArguments = [
'öppna',
]
export const Auth = (props) => {
export const Auth = ({ navigation }: AuthProps) => {
const [argument] = useState(() => {
const argNum = Math.floor(Math.random() * funArguments.length)
return funArguments[argNum]
@ -52,7 +58,7 @@ export const Auth = (props) => {
<Text category="h6" style={styles.subtitle}>
Det {argument} alternativet
</Text>
<Login {...props} />
<Login navigation={navigation} />
</Layout>
</View>
</SafeAreaView>

View File

@ -1,9 +1,10 @@
import { useCalendar } from '@skolplattformen/api-hooks'
import { CalendarItem } from '@skolplattformen/embedded-api'
import { Divider, List, ListItem, Text } from '@ui-kitten/components'
import moment from 'moment'
import 'moment/locale/sv'
import React from 'react'
import { Image, StyleSheet, View } from 'react-native'
import { Image, ListRenderItemInfo, StyleSheet, View } from 'react-native'
import { useChild } from './childContext.component'
import { CalendarOutlineIcon } from './icon.component'
import { SaveToCalendar } from './saveToCalendar.component'
@ -14,16 +15,6 @@ export const Calendar = () => {
const child = useChild()
const { data } = useCalendar(child)
const renderItem = ({ item }) => (
<ListItem
disabled={true}
title={`${item.title}`}
description={`${moment(item.startDate).fromNow()}`}
accessoryLeft={CalendarOutlineIcon}
accessoryRight={() => <SaveToCalendar event={item} />}
/>
)
return !data?.length ? (
<View style={styles.emptyState}>
<Image
@ -35,9 +26,19 @@ export const Calendar = () => {
) : (
<List
contentContainerStyle={styles.contentContainer}
data={data.sort((a, b) => b.startDate < a.startDate)}
data={data.sort((a, b) =>
a.startDate && b.startDate ? b.startDate.localeCompare(a.startDate) : 0
)}
ItemSeparatorComponent={Divider}
renderItem={renderItem}
renderItem={({ item }: ListRenderItemInfo<CalendarItem>) => (
<ListItem
disabled={true}
title={`${item.title}`}
description={`${moment(item.startDate).fromNow()}`}
accessoryLeft={CalendarOutlineIcon}
accessoryRight={() => <SaveToCalendar event={item} />}
/>
)}
style={styles.container}
/>
)

View File

@ -1,4 +1,9 @@
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import {
BottomTabBarOptions,
BottomTabBarProps,
createBottomTabNavigator,
} from '@react-navigation/bottom-tabs'
import { NavigationProp, RouteProp } from '@react-navigation/core'
import {
BottomNavigation,
BottomNavigationTab,
@ -8,7 +13,7 @@ import {
TopNavigationAction,
} from '@ui-kitten/components'
import React from 'react'
import { StyleSheet } from 'react-native'
import { StyleProp, StyleSheet, TextProps } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { studentName } from '../utils/peopleHelpers'
import { Calendar } from './calendar.component'
@ -21,9 +26,20 @@ import {
NewsIcon,
NotificationsIcon,
} from './icon.component'
import { RootStackParamList } from './navigation.component'
import { NewsList } from './newsList.component'
import { NotificationsList } from './notificationsList.component'
interface ChildProps {
navigation: NavigationProp<RootStackParamList, 'Child'>
route: RouteProp<RootStackParamList, 'Child'>
}
interface TabTitleProps {
children: string
style?: StyleProp<TextProps>
}
const { Navigator, Screen } = createBottomTabNavigator()
const NewsScreen = () => {
@ -58,13 +74,16 @@ const ClassmatesScreen = () => {
)
}
const TabTitle = ({ style, children }) => (
const TabTitle = ({ style, children }: TabTitleProps) => (
<Text adjustsFontSizeToFit numberOfLines={1} style={style}>
{children}
</Text>
)
const BottomTabBar = ({ navigation, state }) => (
const BottomTabBar = ({
navigation,
state,
}: BottomTabBarProps<BottomTabBarOptions>) => (
<BottomNavigation
selectedIndex={state.index}
onSelect={(index) => navigation.navigate(state.routeNames[index])}
@ -100,8 +119,8 @@ const TabNavigator = ({ initialRouteName = 'Nyheter' }) => (
</Navigator>
)
export const Child = ({ route, navigation }) => {
const { child, color, initialRouteName } = route.params
export const Child = ({ route, navigation }: ChildProps) => {
const { child, initialRouteName } = route.params
const BackAction = () => (
<TopNavigationAction icon={BackIcon} onPress={navigateBack} />
@ -112,7 +131,7 @@ export const Child = ({ route, navigation }) => {
}
return (
<SafeAreaView style={{ ...styles.wrap, color }}>
<SafeAreaView style={{ ...styles.wrap }}>
<ChildProvider child={child}>
<TopNavigation
title={studentName(child.name)}

View File

@ -1,9 +0,0 @@
import React, { createContext, useContext } from 'react'
export const ChildContext = createContext({})
export const ChildProvider = ({ child, children }) => {
return <ChildContext.Provider value={child}>{children}</ChildContext.Provider>
}
export const useChild = () => useContext(ChildContext)

View File

@ -0,0 +1,19 @@
import { Child } from '@skolplattformen/embedded-api'
import React, { createContext, useContext } from 'react'
interface ChildProviderProps {
child: Child
children: React.ReactNode
}
export const ChildContext = createContext<Child>({
id: '',
sdsId: '',
name: '',
})
export const ChildProvider = ({ child, children }: ChildProviderProps) => {
return <ChildContext.Provider value={child}>{children}</ChildContext.Provider>
}
export const useChild = () => useContext(ChildContext)

View File

@ -1,3 +1,4 @@
import { NavigationProp } from '@react-navigation/core'
import {
useCalendar,
useClassmates,
@ -5,11 +6,13 @@ import {
useNotifications,
useSchedule,
} from '@skolplattformen/api-hooks'
import { Child } from '@skolplattformen/embedded-api'
import { Avatar, Button, Card, Text } from '@ui-kitten/components'
import { RenderProp } from '@ui-kitten/components/devsupport'
import { DateTime } from 'luxon'
import moment from 'moment'
import React from 'react'
import { StyleSheet, View } from 'react-native'
import { StyleSheet, View, ViewProps } from 'react-native'
import { studentName } from '../utils/peopleHelpers'
import {
CalendarOutlineIcon,
@ -17,8 +20,19 @@ import {
NewsIcon,
NotificationsIcon,
} from './icon.component'
import { RootStackParamList } from './navigation.component'
export const ChildListItem = ({ navigation, child, color }) => {
interface ChildListItemProps {
child: Child
color: string
navigation: NavigationProp<RootStackParamList, 'Children'>
}
export const ChildListItem = ({
navigation,
child,
color,
}: ChildListItemProps) => {
// Forces rerender when child.id changes
React.useEffect(() => {}, [child.id])
@ -30,12 +44,12 @@ export const ChildListItem = ({ navigation, child, color }) => {
const { data: calendar, status: calendarStatus } = useCalendar(child)
const { data: schedule } = useSchedule(
child,
DateTime.local(),
DateTime.local().plus({ days: 7 })
DateTime.local().toISO(),
DateTime.local().plus({ days: 7 }).toISO()
)
const notificationsThisWeek = notifications.filter((n) =>
moment(n).isSame('week')
moment(n.dateCreated).isSame('week')
)
const scheduleAndCalendarThisWeek = [
@ -55,23 +69,20 @@ export const ChildListItem = ({ navigation, child, color }) => {
GR: 'Grundskolan',
F: 'Förskoleklass',
}
return child.status
.split(';')
.map((status) => abbrevations[status] || status)
.join(', ')
? child.status
.split(';')
.map((status) => {
const statusAsAbbreviation = status as keyof typeof abbrevations
return abbrevations[statusAsAbbreviation] || status
})
.join(', ')
: null
}
const Header = (props) => (
<View {...props} style={styles.cardHeader}>
<View style={styles.cardAvatar}>
<Avatar source={require('../assets/avatar.png')} shape="square" />
</View>
<View style={styles.cardHeaderText}>
<Text category="h6">{studentName(child.name)}</Text>
<Text category="s1">{`${getClassName()}`}</Text>
</View>
</View>
)
const className = getClassName()
const Footer = () => (
<View style={styles.itemFooter}>
@ -143,17 +154,22 @@ export const ChildListItem = ({ navigation, child, color }) => {
style={styles.card}
appearance="filled"
status={color}
header={Header}
header={(props) => (
<View {...props} style={styles.cardHeader}>
<View style={styles.cardAvatar}>
<Avatar source={require('../assets/avatar.png')} shape="square" />
</View>
<View style={styles.cardHeaderText}>
<Text category="h6">{studentName(child.name)}</Text>
{className ? <Text category="s1">{className}</Text> : null}
</View>
</View>
)}
footer={Footer}
onPress={() => navigation.navigate('Child', { child, color })}
>
{scheduleAndCalendarThisWeek.slice(0, 3).map((calendarItem, i) => (
<Text
appearance="hint"
category="c1"
key={i}
style={{ textColor: styles.loaded(notificationsStatus) }}
>
<Text appearance="hint" category="c1" key={i} style={styles.loaded}>
{`${calendarItem.title}`}
</Text>
))}
@ -213,4 +229,5 @@ const styles = StyleSheet.create({
error: {
color: '#500',
},
pending: {},
})

View File

@ -0,0 +1,163 @@
import { NavigationProp } from '@react-navigation/core'
import { useApi, useChildList } from '@skolplattformen/api-hooks'
import { Child } from '@skolplattformen/embedded-api'
import {
Divider,
Layout,
List,
Spinner,
Text,
TopNavigation,
TopNavigationAction,
} from '@ui-kitten/components'
import React from 'react'
import {
Dimensions,
Image,
ListRenderItemInfo,
SafeAreaView,
StyleSheet,
View,
} from 'react-native'
import ActionSheet from 'rn-actionsheet-module'
import { ChildListItem } from './childListItem.component'
import { SettingsIcon } from './icon.component'
import { RootStackParamList } from './navigation.component'
interface ChildrenProps {
navigation: NavigationProp<RootStackParamList, 'Children'>
}
const { width } = Dimensions.get('window')
const colors = ['primary', 'success', 'info', 'warning', 'danger']
const settingsOptions = ['Logga ut', 'Avbryt']
export const Children = ({ navigation }: ChildrenProps) => {
const { api } = useApi()
const { data: childList, status } = useChildList()
const handleSettingSelection = (index: number) => {
switch (index) {
case 0:
api.logout()
navigation.navigate('Login')
}
}
const settings = () => {
const options = {
cancelButtonIndex: 1,
title: 'Inställningar',
optionsIOS: settingsOptions,
optionsAndroid: settingsOptions,
onCancelAndroidIndex: handleSettingSelection,
}
ActionSheet(options, handleSettingSelection)
}
return (
<SafeAreaView style={styles.topContainer}>
{status === 'loaded' ? (
<>
<TopNavigation
title="Dina barn"
alignment="center"
accessoryRight={() => (
<TopNavigationAction icon={SettingsIcon} onPress={settings} />
)}
/>
<Divider />
<List
contentContainerStyle={styles.childListContainer}
data={childList}
style={styles.childList}
ListEmptyComponent={
<View style={styles.emptyState}>
<Text category="h2">Inga barn</Text>
<Text style={styles.emptyStateDescription}>
Det finns inga barn registrerade för ditt personnummer i
Stockholms Stad
</Text>
<Image
source={require('../assets/children.png')}
style={styles.emptyStateImage}
/>
</View>
}
renderItem={({ item: child, index }: ListRenderItemInfo<Child>) => (
<ChildListItem
child={child}
color={colors[index % colors.length]}
key={child.id}
navigation={navigation}
/>
)}
/>
</>
) : (
<Layout style={styles.loading}>
<Image
source={require('../assets/girls.png')}
style={styles.loadingImage}
/>
<View style={styles.loadingMessage}>
<Spinner size="large" status="warning" />
<Text category="h1" style={styles.loadingText}>
Laddar...
</Text>
</View>
</Layout>
)}
</SafeAreaView>
)
}
const styles = StyleSheet.create({
topContainer: {
flex: 1,
backgroundColor: '#fff',
},
loading: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingImage: {
height: (width / 16) * 9,
width: width,
},
loadingMessage: {
alignItems: 'center',
flexDirection: 'row',
marginTop: 8,
},
loadingText: {
marginLeft: 20,
},
childList: {
flex: 1,
},
childListContainer: {
padding: 20,
},
emptyState: {
backgroundColor: '#fff',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 20,
},
emptyStateDescription: {
lineHeight: 21,
marginTop: 8,
textAlign: 'center',
},
emptyStateImage: {
// 80% size and 16:9 aspect ratio
height: ((width * 0.8) / 16) * 9,
marginTop: 20,
width: width * 0.8,
},
})

View File

@ -1,90 +0,0 @@
// import { useClassmates } from '@skolplattformen/api-hooks'
import { Card, Text } from '@ui-kitten/components'
import React from 'react'
import { View, StyleSheet } from 'react-native'
// import { fullName, guardians, sortByFirstName } from '../utils/peopleHelpers'
// import { useChild } from './childContext.component'
// import { ContactMenu } from './contactMenu.component'
export const Classmates = () => {
// const child = useChild()
/*
const { data } = useClassmates(child)
const renderItemIcon = (props) => <Icon {...props} name="people-outline" />
const [selected, setSelected] = React.useState()
const renderItem = ({ item, index }) => (
<ListItem
accessibilityLabel={`Barn ${index + 1}`}
title={fullName(item)}
onPress={() => setSelected(item)}
description={guardians(item.guardians)}
accessoryLeft={renderItemIcon}
accessoryRight={() => (
<ContactMenu
contact={item}
selected={item === selected}
setSelected={setSelected}
/>
)}
/>
)
return (
<List
style={styles.container}
data={sortByFirstName(data)}
ItemSeparatorComponent={Divider}
ListHeaderComponent={
<Text category="h5" style={styles.listHeader}>
{data?.length ? `Klass ${data[0].className}` : 'Klass'}
</Text>
}
renderItem={renderItem}
contentContainerStyle={styles.contentContainer}
/>
)
*/
const cardHeader = (props) => {
return (
<View style={styles.topContainer}>
<Text category="h6">Klasslistan ej tillgänglig</Text>
</View>
)
}
return (
<View style={styles.container}>
<Card header={cardHeader} style={styles.contentContainer}>
<Text>
Klasslista kan tyvärr inte visas längre. Vi jobbar att lösa det,
och återkommer med information när vi vet mer.
</Text>
</Card>
</View>
)
}
const styles = StyleSheet.create({
container: {
height: '100%',
width: '100%',
},
contentContainer: {
margin: 10,
justifyContent: 'flex-start',
},
topContainer: {
margin: 5,
flexDirection: 'row',
justifyContent: 'space-between',
},
listHeader: {
backgroundColor: '#fff',
paddingTop: 10,
paddingLeft: 15,
},
})

View File

@ -0,0 +1,45 @@
import { Card, Text } from '@ui-kitten/components'
import React from 'react'
import { View, StyleSheet } from 'react-native'
export const Classmates = () => {
const cardHeader = () => {
return (
<View style={styles.topContainer}>
<Text category="h6">Klasslistan ej tillgänglig</Text>
</View>
)
}
return (
<View style={styles.container}>
<Card header={cardHeader} style={styles.contentContainer}>
<Text>
Klasslista kan tyvärr inte visas längre. Vi jobbar att lösa det,
och återkommer med information när vi vet mer.
</Text>
</Card>
</View>
)
}
const styles = StyleSheet.create({
container: {
height: '100%',
width: '100%',
},
contentContainer: {
margin: 10,
justifyContent: 'flex-start',
},
topContainer: {
margin: 5,
flexDirection: 'row',
justifyContent: 'space-between',
},
listHeader: {
backgroundColor: '#fff',
paddingTop: 10,
paddingLeft: 15,
},
})

View File

@ -1,7 +1,7 @@
import { Icon } from '@ui-kitten/components'
import React from 'react'
const uiIcon = (name) => (props) => <Icon {...props} name={name} />
const uiIcon = (name: string) => (props: any) => <Icon {...props} name={name} />
export const BackIcon = uiIcon('arrow-back')
export const CalendarOutlineIcon = uiIcon('calendar-outline')

View File

@ -1,33 +0,0 @@
import { useApi } from '@skolplattformen/api-hooks'
import React, { useEffect, useState } from 'react'
import { Image as ImageBase } from 'react-native'
export const Image = ({ src, style }) => {
const { api } = useApi()
const [headers, setHeaders] = useState()
const getHeaders = async (url) => {
// eslint-disable-next-line no-shadow
const { headers } = await api.getSession(url)
setHeaders(headers)
}
useEffect(() => {
getHeaders(src)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [src])
return (
<>
{headers && (
<ImageBase
source={{
uri: src,
headers,
}}
style={style}
/>
)}
</>
)
}

View File

@ -0,0 +1,33 @@
import { useApi } from '@skolplattformen/api-hooks'
import React, { useEffect, useState } from 'react'
import { Image as ImageBase, ImageStyle, StyleProp } from 'react-native'
interface ImageProps {
src: string
style: StyleProp<ImageStyle>
}
export const Image = ({ src, style }: ImageProps) => {
const { api } = useApi()
const [headers, setHeaders] = useState()
const getHeaders = async (url: string) => {
const { headers: newHeaders } = await api.getSession(url)
setHeaders(newHeaders)
}
useEffect(() => {
getHeaders(src)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [src])
return headers ? (
<ImageBase
source={{
uri: src,
headers,
}}
style={style}
/>
) : null
}

View File

@ -1,10 +1,15 @@
import { Text } from '@ui-kitten/components'
import React from 'react'
import { Linking, StyleSheet } from 'react-native'
import MarkdownBase from 'react-native-markdown-display'
import MarkdownBase, { RenderRules } from 'react-native-markdown-display'
import { Image } from './image.component'
const rules = {
interface MarkdownProps {
children: React.ReactNode
style?: StyleSheet.NamedStyles<any>
}
const rules: RenderRules = {
image: (node) => {
const { src } = node.attributes
const url = src.startsWith('/')
@ -13,19 +18,23 @@ const rules = {
return <Image key={src} src={url} style={styles.markdownImage} />
},
link: (node, children, _parent, styles) => {
return (
<Text
key={node.key}
style={styles.link}
onPress={() => Linking.openURL(node.attributes.href)}
>
{children}
</Text>
)
if (typeof children === 'string') {
return (
<Text
key={node.key}
style={styles.link}
onPress={() => Linking.openURL(node.attributes.href)}
>
{children}
</Text>
)
}
return null
},
}
export const Markdown = ({ style, children }) => {
export const Markdown = ({ style, children }: MarkdownProps) => {
return (
<MarkdownBase rules={rules} style={style}>
{children}

View File

@ -7,7 +7,12 @@ import { SafeAreaView } from 'react-native-safe-area-context'
import { WebView } from 'react-native-webview'
import { CloseIcon } from './icon.component'
export const ModalWebView = ({ url, onClose }) => {
interface ModalWebViewProps {
url: string
onClose: () => void
}
export const ModalWebView = ({ url, onClose }: ModalWebViewProps) => {
const [modalVisible, setModalVisible] = React.useState(true)
const { api } = useApi()
const [headers, setHeaders] = useState()

View File

@ -0,0 +1,59 @@
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import React from 'react'
import { StatusBar } from 'react-native'
import { schema } from '../app.json'
import Absence from './absence.component'
import { Child } from './child.component'
import { Children } from './children.component'
import { Auth } from './auth.component'
import { NewsItem } from './newsItem.component'
import {
Child as ChildType,
NewsItem as NewsItemType,
} from '@skolplattformen/embedded-api'
export type RootStackParamList = {
Login: undefined
Children: undefined
Child: {
child: ChildType
color: string
initialRouteName?: string
}
NewsItem: { newsItem: NewsItemType; child: ChildType }
Absence: { child: ChildType }
}
export type SigninStackParamList = {
Login: undefined
}
const { Navigator, Screen } = createStackNavigator()
const HomeNavigator = () => (
<Navigator headerMode="none">
<Screen name="Login" component={Auth} />
<Screen name="Children" component={Children} />
<Screen name="Child" component={Child} />
<Screen name="NewsItem" component={NewsItem} />
<Screen name="Absence" component={Absence} />
</Navigator>
)
const linking = {
prefixes: [schema],
config: {
screens: {
Login: 'login',
},
},
}
export const AppNavigator = () => {
return (
<NavigationContainer linking={linking}>
<StatusBar />
<HomeNavigator />
</NavigationContainer>
)
}

View File

@ -1,3 +1,5 @@
import { RouteProp } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { useNewsDetails } from '@skolplattformen/api-hooks'
import {
Divider,
@ -12,13 +14,20 @@ import { SafeAreaView, ScrollView, StyleSheet, View } from 'react-native'
import { BackIcon } from './icon.component'
import { Image } from './image.component'
import { Markdown } from './markdown.component'
import { RootStackParamList } from './navigation.component'
const displayDate = (date) =>
interface NewsItemProps {
navigation: StackNavigationProp<RootStackParamList, 'NewsItem'>
route: RouteProp<RootStackParamList, 'NewsItem'>
}
const displayDate = (date: string | undefined) =>
moment(date).locale('sv').format('DD MMM. YYYY HH:mm')
const dateIsValid = (date) => moment(date, moment.ISO_8601).isValid()
const dateIsValid = (date: string | undefined) =>
moment(date, moment.ISO_8601).isValid()
export const NewsItem = ({ navigation, route }) => {
export const NewsItem = ({ navigation, route }: NewsItemProps) => {
const { newsItem, child } = route.params
const { data } = useNewsDetails(child, newsItem)

View File

@ -1,4 +1,5 @@
import { useNavigation } from '@react-navigation/native'
import { NewsItem } from '@skolplattformen/embedded-api'
import { DateTime } from 'luxon'
import React from 'react'
import { Dimensions, StyleSheet, Text, View } from 'react-native'
@ -6,22 +7,29 @@ import { TouchableOpacity } from 'react-native-gesture-handler'
import { useChild } from './childContext.component'
import { Image } from './image.component'
interface NewsListItemProps {
item: NewsItem
}
const { width } = Dimensions.get('window')
export const NewsListItem = ({ item }) => {
export const NewsListItem = ({ item }: NewsListItemProps) => {
const navigation = useNavigation()
const child = useChild()
const hasDate = item.published || item.modified
const displayDate = DateTime.fromISO(
item.published || item.modified
).toRelative({ locale: 'sv', style: 'long' })
const displayDate = hasDate
? DateTime.fromISO(hasDate).toRelative({ locale: 'sv', style: 'long' })
: null
return (
<TouchableOpacity
onPress={() => navigation.navigate('NewsItem', { newsItem: item, child })}
>
<View style={styles.card}>
{width > 320 && <Image src={item.fullImageUrl} style={styles.image} />}
{width > 320 && item.fullImageUrl ? (
<Image src={item.fullImageUrl} style={styles.image} />
) : null}
<View style={styles.text}>
<View>
<Text style={styles.title}>{item.header}</Text>
@ -30,12 +38,7 @@ export const NewsListItem = ({ item }) => {
{item.author && displayDate ? ' • ' : ''}
{displayDate}
</Text>
<Text
ellipsizeMode="tail"
numberOfLines={2}
category="s2"
style={styles.intro}
>
<Text ellipsizeMode="tail" numberOfLines={2} style={styles.intro}>
{item.intro}
</Text>
</View>

View File

@ -1,10 +1,15 @@
import { Notification as NotificationType } from '@skolplattformen/embedded-api'
import { Card, Text } from '@ui-kitten/components'
import { DateTime } from 'luxon'
import React from 'react'
import { StyleSheet, View } from 'react-native'
import { ModalWebView } from './modalWebView.component'
export const Notification = ({ item }) => {
interface NotificationProps {
item: NotificationType
}
export const Notification = ({ item }: NotificationProps) => {
const [isOpen, setIsOpen] = React.useState(false)
const open = () => setIsOpen(true)
const close = () => setIsOpen(false)

View File

@ -1,11 +1,18 @@
import { Button, MenuItem, OverflowMenu } from '@ui-kitten/components'
import React from 'react'
import { StyleSheet } from 'react-native'
import RNCalendarEvents from 'react-native-calendar-events'
import RNCalendarEvents, {
CalendarEventWritable,
} from 'react-native-calendar-events'
import { CalendarOutlineIcon, MoreIcon } from './icon.component'
import Toast from 'react-native-simple-toast'
import { CalendarItem } from '@skolplattformen/embedded-api'
export const SaveToCalendar = ({ event }) => {
interface SaveToCalendarProps {
event: CalendarItem
}
export const SaveToCalendar = ({ event }: SaveToCalendarProps) => {
const [visible, setVisible] = React.useState(false)
const renderToggleButton = () => (
@ -21,7 +28,14 @@ export const SaveToCalendar = ({ event }) => {
setVisible(false)
}
const toast = (text) => Toast.showWithGravity(text, Toast.SHORT, Toast.BOTTOM)
const toast = (text: string) =>
Toast.showWithGravity(text, Toast.SHORT, Toast.BOTTOM)
function removeEmptyValues<T extends object>(obj: T) {
return Object.fromEntries(
Object.entries(obj).filter(([_, v]) => v != null)
) as { [K in keyof T]: any }
}
const requestPermissionsAndSave = async ({
title,
@ -29,21 +43,23 @@ export const SaveToCalendar = ({ event }) => {
endDate,
location,
description: notes,
}) => {
}: CalendarItem) => {
const auth = await RNCalendarEvents.requestPermissions()
if (auth === 'authorized') {
try {
const details = {
startDate: new Date(startDate).toISOString(),
endDate: new Date(endDate).toISOString(),
startDate: startDate
? new Date(startDate).toISOString()
: new Date().toISOString(),
endDate: endDate
? new Date(endDate).toISOString()
: new Date().toISOString(),
location,
notes,
}
const detailsWithoutEmpty = Object.fromEntries(
Object.entries(details).filter(([_, v]) => v != null)
)
const detailsWithoutEmpty = removeEmptyValues(details)
await RNCalendarEvents.saveEvent(title, detailsWithoutEmpty)

View File

@ -4,12 +4,13 @@
"scripts": {
"android": "react-native run-android",
"ios": "react-native-fix-image && react-native run-ios",
"lint": "eslint .",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"pod": "npx pod-install",
"start": "react-native start",
"test": "is-ci-cli test:ci test:watch",
"test:ci": "jest",
"test:watch": "jest --watch"
"test:watch": "jest --watch",
"typecheck": "tsc --watch"
},
"dependencies": {
"@eva-design/eva": "2.0.0",
@ -58,6 +59,11 @@
"@testing-library/jest-native": "^4.0.1",
"@testing-library/react-hooks": "^5.1.0",
"@testing-library/react-native": "7.1.0",
"@types/jest": "^26.0.22",
"@types/jsuri": "^1.3.30",
"@types/luxon": "^1.26.2",
"@types/markdown-it": "^12.0.1",
"@types/react-native": "^0.64.1",
"@ui-kitten/metro-config": "5.0.0",
"babel-jest": "^26.6.3",
"eslint": "^7.21.0",
@ -69,7 +75,8 @@
"metro-react-native-babel-preset": "^0.65.2",
"mockdate": "^3.0.2",
"prettier": "^2.2.1",
"react-test-renderer": "16.13.1"
"react-test-renderer": "16.13.1",
"typescript": "^4.2.3"
},
"private": true,
"resolutions": {

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"lib": [ "es2019"],
"allowJs": true,
"jsx": "react-native",
"noEmit": true,
"isolatedModules": true,
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": false,
"resolveJsonModule": true
},
"exclude": [
"node_modules", "babel.config.js", "metro.config.js", "jest.config.js"
]
}

1
packages/app/types.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'rn-actionsheet-module'

View File

@ -1715,6 +1715,11 @@
resolved "https://registry.yarnpkg.com/@types/he/-/he-1.1.1.tgz#19e14033c4ee8f1a702c74dcc6182664839ac2b7"
integrity sha512-jpzrsR1ns0n3kyWt92QfOUQhIuJGQ9+QGa7M62rO6toe98woQjnsnzjdMtsQXCdvjjmqjS2ZBCC7xKw0cdzU+Q==
"@types/highlight.js@^9.7.0":
version "9.12.4"
resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.4.tgz#8c3496bd1b50cc04aeefd691140aa571d4dbfa34"
integrity sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@ -1742,16 +1747,53 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@^26.0.22":
version "26.0.22"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.22.tgz#8308a1debdf1b807aa47be2838acdcd91e88fbe6"
integrity sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==
dependencies:
jest-diff "^26.0.0"
pretty-format "^26.0.0"
"@types/json-schema@^7.0.3":
version "7.0.7"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
"@types/jsuri@^1.3.30":
version "1.3.30"
resolved "https://registry.yarnpkg.com/@types/jsuri/-/jsuri-1.3.30.tgz#2feb9768b43fe0494a60d39d78172bd31a4e82a9"
integrity sha512-n3RoOl8LHzDX7gmgxVWU09cMiUn1chW0J70N6oOptOoLt8OWvyq8lpW9Mj4xvzHqJEiOMR+J1okPSL1XeBz1uw==
"@types/linkify-it@*":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.1.tgz#4d26a9efe3aa2caf829234ec5a39580fc88b6001"
integrity sha512-pQv3Sygwxxh6jYQzXaiyWDAHevJqWtqDUv6t11Sa9CPGiXny66II7Pl6PR8QO5OVysD6HYOkHMeBgIjLnk9SkQ==
"@types/lodash@^4.14.165":
version "4.14.168"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==
"@types/luxon@^1.26.2":
version "1.26.2"
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.26.2.tgz#656c24c1af3d41b8854700dc94ed556b9b6ce2f8"
integrity sha512-2pvzy4LuxBMBBLAbml6PDcJPiIeZQ0Hqj3PE31IxkNI250qeoRMDovTrHXeDkIL4auvtarSdpTkLHs+st43EYQ==
"@types/markdown-it@^12.0.1":
version "12.0.1"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.0.1.tgz#8391e19fea4796ff863edda55800c7e669beb358"
integrity sha512-mHfT8j/XkPb1uLEfs0/C3se6nd+webC2kcqcy8tgcVr0GDEONv/xaQzAN+aQvkxQXk/jC0Q6mPS+0xhFwRF35g==
dependencies:
"@types/highlight.js" "^9.7.0"
"@types/linkify-it" "*"
"@types/mdurl" "*"
"@types/mdurl@*":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
"@types/node@*":
version "14.14.31"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
@ -1789,6 +1831,13 @@
dependencies:
"@types/react" "*"
"@types/react-native@^0.64.1":
version "0.64.1"
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.64.1.tgz#03c897d9567c357922d06d4ed159354c81a3fe5c"
integrity sha512-5VUk12LTxawpcRLbzzTtbcFVHCw8ro7a9dF2OsJSwNiMZAgziyqZbAv1Sw1TgoFlfX0rOxXv/tMMJ1r1HVZ8Og==
dependencies:
"@types/react" "*"
"@types/react-test-renderer@>=16.9.0":
version "17.0.1"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b"
@ -5082,7 +5131,7 @@ jest-diff@^25.5.0:
jest-get-type "^25.2.6"
pretty-format "^25.5.0"
jest-diff@^26.6.2:
jest-diff@^26.0.0, jest-diff@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394"
integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==
@ -7318,7 +7367,7 @@ pretty-format@^25.1.0, pretty-format@^25.2.0, pretty-format@^25.5.0:
ansi-styles "^4.0.0"
react-is "^16.12.0"
pretty-format@^26.0.1, pretty-format@^26.6.2:
pretty-format@^26.0.0, pretty-format@^26.0.1, pretty-format@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93"
integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==
@ -8883,6 +8932,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
ua-parser-js@^0.7.18:
version "0.7.24"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.24.tgz#8d3ecea46ed4f1f1d63ec25f17d8568105dc027c"