refactor(react 18/RN 0.72 update)
Co-authored-by: Sebastian Palmqvist <PalmN72@users.noreply.github.com>
|
@ -1,71 +0,0 @@
|
||||||
# OSX
|
|
||||||
#
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Xcode
|
|
||||||
#
|
|
||||||
build/
|
|
||||||
*.pbxuser
|
|
||||||
!default.pbxuser
|
|
||||||
*.mode1v3
|
|
||||||
!default.mode1v3
|
|
||||||
*.mode2v3
|
|
||||||
!default.mode2v3
|
|
||||||
*.perspectivev3
|
|
||||||
!default.perspectivev3
|
|
||||||
xcuserdata
|
|
||||||
*.xccheckout
|
|
||||||
*.moved-aside
|
|
||||||
DerivedData
|
|
||||||
*.hmap
|
|
||||||
*.ipa
|
|
||||||
*.xcuserstate
|
|
||||||
ios/.xcode.env.local
|
|
||||||
|
|
||||||
# Android/IntelliJ
|
|
||||||
#
|
|
||||||
build/
|
|
||||||
.idea
|
|
||||||
.gradle
|
|
||||||
local.properties
|
|
||||||
*.iml
|
|
||||||
*.hprof
|
|
||||||
.cxx/
|
|
||||||
!debug.keystore
|
|
||||||
release/
|
|
||||||
|
|
||||||
# node.js
|
|
||||||
#
|
|
||||||
node_modules/
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
|
|
||||||
# fastlane
|
|
||||||
#
|
|
||||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
|
||||||
# screenshots whenever they are needed.
|
|
||||||
# For more information about the recommended setup visit:
|
|
||||||
# https://docs.fastlane.tools/best-practices/source-control/
|
|
||||||
|
|
||||||
**/fastlane/report.xml
|
|
||||||
**/fastlane/Preview.html
|
|
||||||
**/fastlane/screenshots
|
|
||||||
**/fastlane/test_output
|
|
||||||
|
|
||||||
# Bundle artifact
|
|
||||||
*.jsbundle
|
|
||||||
|
|
||||||
# Ruby / CocoaPods
|
|
||||||
/ios/Pods/
|
|
||||||
/vendor/bundle/
|
|
||||||
|
|
||||||
# Temporary files created by Metro to check the health of the file watcher
|
|
||||||
.metro-health-check*
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
|
|
||||||
libraries.json
|
|
||||||
|
|
||||||
keys.json
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"printWidth": 80,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"bracketSameLine": false
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
|
@ -1,111 +0,0 @@
|
||||||
import * as eva from '@eva-design/eva'
|
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
||||||
import { ApiProvider, Reporter } from './libs/hooks/src'
|
|
||||||
import { ApplicationProvider, IconRegistry, Text } from '@ui-kitten/components'
|
|
||||||
import { EvaIconsPack } from '@ui-kitten/eva-icons'
|
|
||||||
import React from 'react'
|
|
||||||
import { StatusBar, useColorScheme, View } from 'react-native'
|
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
|
||||||
import { AppNavigator } from './components/navigation.component'
|
|
||||||
import { FeatureProvider } from './context/feature/featureContext'
|
|
||||||
import { LanguageProvider } from './context/language/languageContext'
|
|
||||||
import { SchoolPlatformProvider } from './context/schoolPlatform/schoolPlatformContext'
|
|
||||||
import { schoolPlatforms } from './data/schoolPlatforms'
|
|
||||||
import { default as customMapping } from './design/mapping.json'
|
|
||||||
import { darkTheme, lightTheme } from './design/themes'
|
|
||||||
import useSettingsStorage from './hooks/useSettingsStorage'
|
|
||||||
import { translations } from './utils/translation'
|
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
|
||||||
|
|
||||||
const reporter: Reporter | undefined = __DEV__
|
|
||||||
? {
|
|
||||||
log: (message: string) => console.log(message),
|
|
||||||
error: (error: Error, label?: string) => console.log(label, error),
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
if (__DEV__) {
|
|
||||||
const DevMenu = require('react-native-dev-menu')
|
|
||||||
DevMenu.addItem('Clear AsyncStorage from all contents', () =>
|
|
||||||
AsyncStorage.clear().then(() => logAsyncStorage())
|
|
||||||
)
|
|
||||||
DevMenu.addItem('Log AsyncStorage contents', () => logAsyncStorage())
|
|
||||||
}
|
|
||||||
|
|
||||||
const safeJsonParse = (maybeJson: string) => {
|
|
||||||
if (maybeJson) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(maybeJson)
|
|
||||||
} catch (error) {
|
|
||||||
return maybeJson
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'null'
|
|
||||||
}
|
|
||||||
|
|
||||||
const logAsyncStorage = async () => {
|
|
||||||
const allKeys = await AsyncStorage.getAllKeys()
|
|
||||||
const keysAndValues = await AsyncStorage.multiGet(allKeys)
|
|
||||||
console.log('*** AsyncStorage contents:')
|
|
||||||
keysAndValues.forEach((keyAndValue) => {
|
|
||||||
console.log(
|
|
||||||
keyAndValue[0],
|
|
||||||
'=>',
|
|
||||||
keyAndValue[1] ? safeJsonParse(keyAndValue[1]) : 'null'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
console.log('***')
|
|
||||||
}
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const [usingSystemTheme] = useSettingsStorage('usingSystemTheme')
|
|
||||||
const [currentSchoolPlatform] = useSettingsStorage('currentSchoolPlatform')
|
|
||||||
const [theme] = useSettingsStorage('theme')
|
|
||||||
const systemTheme = useColorScheme()
|
|
||||||
const colorScheme = usingSystemTheme ? systemTheme : theme
|
|
||||||
|
|
||||||
const platform = schoolPlatforms.find((pf) => pf.id === currentSchoolPlatform)
|
|
||||||
|
|
||||||
if (!platform) {
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<Text>ERROR</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FeatureProvider features={platform.features}>
|
|
||||||
<SchoolPlatformProvider>
|
|
||||||
<ApiProvider
|
|
||||||
api={platform.api}
|
|
||||||
storage={AsyncStorage}
|
|
||||||
reporter={reporter}
|
|
||||||
>
|
|
||||||
<SafeAreaProvider>
|
|
||||||
<StatusBar
|
|
||||||
backgroundColor={colorScheme === 'dark' ? '#2E3137' : '#FFF'}
|
|
||||||
barStyle={
|
|
||||||
colorScheme === 'dark' ? 'light-content' : 'dark-content'
|
|
||||||
}
|
|
||||||
translucent
|
|
||||||
/>
|
|
||||||
<IconRegistry icons={EvaIconsPack} />
|
|
||||||
<ApplicationProvider
|
|
||||||
{...eva}
|
|
||||||
// @ts-expect-error Unknown error
|
|
||||||
customMapping={customMapping}
|
|
||||||
theme={colorScheme === 'dark' ? darkTheme : lightTheme}
|
|
||||||
>
|
|
||||||
<LanguageProvider cache={true} data={translations}>
|
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
||||||
<AppNavigator />
|
|
||||||
</GestureHandlerRootView>
|
|
||||||
</LanguageProvider>
|
|
||||||
</ApplicationProvider>
|
|
||||||
</SafeAreaProvider>
|
|
||||||
</ApiProvider>
|
|
||||||
</SchoolPlatformProvider>
|
|
||||||
</FeatureProvider>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
|
|
||||||
|
|
||||||
# Getting Started
|
|
||||||
|
|
||||||
>**Note**: Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions till "Creating a new application" step, before proceeding.
|
|
||||||
|
|
||||||
## Getting started with Development
|
|
||||||
|
|
||||||
### Please use node version 16 or higher
|
|
||||||
|
|
||||||
To clone and build the project, you first need to install [git](https://git-scm.com/), [node](https://nodejs.org/en/) and [npm](https://docs.npmjs.com/cli/v8/commands/npm-install).
|
|
||||||
|
|
||||||
Clone the repo with
|
|
||||||
```bash
|
|
||||||
$ git clone https://github.com/Home-Biz-LLS/skolplattformen-react-native
|
|
||||||
```
|
|
||||||
|
|
||||||
Install dependencies
|
|
||||||
```bash
|
|
||||||
cd apps/skolplattformen-app-new/ && npm i
|
|
||||||
```
|
|
||||||
|
|
||||||
### iOS
|
|
||||||
|
|
||||||
If you wanna run the iOS app, you need to setup a couple of things first, we have a guide that will assist you in getting started with the iOS app. A Mac is required to build projects with native code for iOS so we do not have support for Linux / Windows.
|
|
||||||
|
|
||||||
* [Mac OS](https://reactnative.dev/docs/environment-setup)
|
|
||||||
|
|
||||||
#### Step 1
|
|
||||||
Make sure you have **Xcode** installed
|
|
||||||
|
|
||||||
|
|
||||||
#### Step 2
|
|
||||||
Make sure **CocoaPods** is installed (you can do it easily with homebrew)
|
|
||||||
|
|
||||||
* [CocoaPods homebrew](https://formulae.brew.sh/formula/cocoapods)
|
|
||||||
* [CocoaPods](https://guides.cocoapods.org/using/getting-started.html)
|
|
||||||
|
|
||||||
|
|
||||||
#### Step 3
|
|
||||||
```bash
|
|
||||||
cd apps/skolplattformen-app-new/ios && pod install
|
|
||||||
```
|
|
||||||
|
|
||||||
If you already setup everything, go into the
|
|
||||||
|
|
||||||
"skolplattformen-app-new" directory
|
|
||||||
|
|
||||||
Start the metro
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run start
|
|
||||||
```
|
|
||||||
|
|
||||||
then
|
|
||||||
|
|
||||||
```
|
|
||||||
i
|
|
||||||
```
|
|
||||||
to Start iOS app
|
|
||||||
|
|
||||||
OR
|
|
||||||
|
|
||||||
Start the iOS app directly
|
|
||||||
```
|
|
||||||
npm run ios
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Android
|
|
||||||
|
|
||||||
If you wanna run the Android app, you need to setup a couple of things first, we have created three different guides depending on your operating system.
|
|
||||||
|
|
||||||
* [Mac OS](/docs/android_mac.md)
|
|
||||||
* [Windows](/docs/android_windows.md)
|
|
||||||
* [Linux](/docs/android_linux.md)
|
|
||||||
|
|
||||||
If you already setup everything, you just need to run the following command in the project root:
|
|
||||||
|
|
||||||
```
|
|
||||||
yarn run android
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
export {
|
|
||||||
default,
|
|
||||||
useAsyncStorage,
|
|
||||||
} from '@react-native-async-storage/async-storage/jest/async-storage-mock'
|
|
|
@ -1,40 +0,0 @@
|
||||||
const getLocales = () => [
|
|
||||||
{ countryCode: 'EN', languageTag: 'en-US', languageCode: 'en', isRTL: false },
|
|
||||||
{ countryCode: 'SE', languageTag: 'sv-SE', languageCode: 'sv', isRTL: false },
|
|
||||||
]
|
|
||||||
|
|
||||||
const findBestAvailableLanguage = jest.fn(() => ({
|
|
||||||
languageTag: 'sv',
|
|
||||||
isRTL: false,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const getNumberFormatSettings = () => ({
|
|
||||||
decimalSeparator: '.',
|
|
||||||
groupingSeparator: ',',
|
|
||||||
})
|
|
||||||
|
|
||||||
const getCalendar = () => 'gregorian'
|
|
||||||
const getCountry = () => 'SE'
|
|
||||||
const getCurrencies = () => ['USD', 'SEK']
|
|
||||||
const getTemperatureUnit = () => 'celsius'
|
|
||||||
const getTimeZone = () => 'Europe/Stockholm'
|
|
||||||
const uses24HourClock = () => true
|
|
||||||
const usesMetricSystem = () => true
|
|
||||||
|
|
||||||
const addEventListener = jest.fn()
|
|
||||||
const removeEventListener = jest.fn()
|
|
||||||
|
|
||||||
export {
|
|
||||||
findBestAvailableLanguage,
|
|
||||||
getLocales,
|
|
||||||
getNumberFormatSettings,
|
|
||||||
getCalendar,
|
|
||||||
getCountry,
|
|
||||||
getCurrencies,
|
|
||||||
getTemperatureUnit,
|
|
||||||
getTimeZone,
|
|
||||||
uses24HourClock,
|
|
||||||
usesMetricSystem,
|
|
||||||
addEventListener,
|
|
||||||
removeEventListener,
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
// __mocks__/react-native-safe-area-context.js
|
|
||||||
import React from 'react'
|
|
||||||
import { View } from 'react-native'
|
|
||||||
|
|
||||||
const inset = {
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SafeAreaProvider = ({ children }) => children
|
|
||||||
|
|
||||||
export const SafeAreaConsumer = ({ children }) => children(inset)
|
|
||||||
|
|
||||||
export const SafeAreaView = ({ children }) => (
|
|
||||||
<View style={inset}>{children}</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const useSafeAreaInsets = () => inset
|
|
|
@ -1,129 +0,0 @@
|
||||||
apply plugin: "com.android.application"
|
|
||||||
apply plugin: "com.facebook.react"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the configuration block to customize your React Native Android app.
|
|
||||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
|
||||||
*/
|
|
||||||
react {
|
|
||||||
/* Folders */
|
|
||||||
// The root of your project, i.e. where "package.json" lives. Default is '..'
|
|
||||||
// root = file("../")
|
|
||||||
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
|
|
||||||
// reactNativeDir = file("../node_modules/react-native")
|
|
||||||
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
|
|
||||||
// codegenDir = file("../node_modules/@react-native/codegen")
|
|
||||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
|
||||||
// cliFile = file("../node_modules/react-native/cli.js")
|
|
||||||
|
|
||||||
/* Variants */
|
|
||||||
// The list of variants to that are debuggable. For those we're going to
|
|
||||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
|
||||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
|
||||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
|
||||||
|
|
||||||
/* Bundling */
|
|
||||||
// A list containing the node command and its flags. Default is just 'node'.
|
|
||||||
// nodeExecutableAndArgs = ["node"]
|
|
||||||
//
|
|
||||||
// The command to run when bundling. By default is 'bundle'
|
|
||||||
// bundleCommand = "ram-bundle"
|
|
||||||
//
|
|
||||||
// The path to the CLI configuration file. Default is empty.
|
|
||||||
// bundleConfig = file(../rn-cli.config.js)
|
|
||||||
//
|
|
||||||
// The name of the generated asset file containing your JS bundle
|
|
||||||
// bundleAssetName = "MyApplication.android.bundle"
|
|
||||||
//
|
|
||||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
|
||||||
// entryFile = file("../js/MyApplication.android.js")
|
|
||||||
//
|
|
||||||
// A list of extra flags to pass to the 'bundle' commands.
|
|
||||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
|
||||||
// extraPackagerArgs = []
|
|
||||||
|
|
||||||
/* Hermes Commands */
|
|
||||||
// The hermes compiler command to run. By default it is 'hermesc'
|
|
||||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
|
||||||
//
|
|
||||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
|
||||||
// hermesFlags = ["-O", "-output-source-map"]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
|
||||||
*/
|
|
||||||
def enableProguardInReleaseBuilds = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The preferred build flavor of JavaScriptCore (JSC)
|
|
||||||
*
|
|
||||||
* For example, to use the international variant, you can use:
|
|
||||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
|
||||||
*
|
|
||||||
* The international variant includes ICU i18n library and necessary data
|
|
||||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
|
||||||
* give correct results when using with locales other than en-US. Note that
|
|
||||||
* this variant is about 6MiB larger per architecture than default.
|
|
||||||
*/
|
|
||||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
|
||||||
|
|
||||||
android {
|
|
||||||
ndkVersion rootProject.ext.ndkVersion
|
|
||||||
|
|
||||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
|
||||||
|
|
||||||
namespace "com.oppna_skolplattformen_new.app"
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.oppna_skolplattformen_new.app"
|
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
|
||||||
versionCode 4
|
|
||||||
versionName "1.0"
|
|
||||||
}
|
|
||||||
signingConfigs {
|
|
||||||
debug {
|
|
||||||
storeFile file('debug.keystore')
|
|
||||||
storePassword 'android'
|
|
||||||
keyAlias 'androiddebugkey'
|
|
||||||
keyPassword 'android'
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
storeFile file('release.jks')
|
|
||||||
storePassword 'leeandseb'
|
|
||||||
keyAlias 'upload'
|
|
||||||
keyPassword 'leeandseb'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
// Caution! In production, you need to generate your own keystore file.
|
|
||||||
// see https://reactnative.dev/docs/signed-apk-android.
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
minifyEnabled true
|
|
||||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// The version of react-native is set by the React Native Gradle Plugin
|
|
||||||
implementation("com.facebook.react:react-android")
|
|
||||||
|
|
||||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
|
||||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
|
||||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
|
||||||
}
|
|
||||||
|
|
||||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
|
|
||||||
if (hermesEnabled.toBoolean()) {
|
|
||||||
implementation("com.facebook.react:hermes-android")
|
|
||||||
} else {
|
|
||||||
implementation jscFlavor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Add project specific ProGuard rules here.
|
|
||||||
# By default, the flags in this file are appended to flags specified
|
|
||||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
|
@ -1,13 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:usesCleartextTraffic="true"
|
|
||||||
tools:targetApi="28"
|
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
|
||||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false" />
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
|
@ -1,75 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
|
||||||
* directory of this source tree.
|
|
||||||
*/
|
|
||||||
package com.oppna_skolplattformen_new.app;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
|
||||||
import com.facebook.flipper.android.utils.FlipperUtils;
|
|
||||||
import com.facebook.flipper.core.FlipperClient;
|
|
||||||
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
|
|
||||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
|
||||||
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
|
|
||||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
|
||||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
|
||||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
|
|
||||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
|
|
||||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
|
||||||
import com.facebook.react.ReactInstanceEventListener;
|
|
||||||
import com.facebook.react.ReactInstanceManager;
|
|
||||||
import com.facebook.react.bridge.ReactContext;
|
|
||||||
import com.facebook.react.modules.network.NetworkingModule;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class responsible of loading Flipper inside your React Native application. This is the debug
|
|
||||||
* flavor of it. Here you can add your own plugins and customize the Flipper setup.
|
|
||||||
*/
|
|
||||||
public class ReactNativeFlipper {
|
|
||||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
|
||||||
if (FlipperUtils.shouldEnableFlipper(context)) {
|
|
||||||
final FlipperClient client = AndroidFlipperClient.getInstance(context);
|
|
||||||
|
|
||||||
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
|
|
||||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
|
||||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
|
||||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
|
||||||
|
|
||||||
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
|
|
||||||
NetworkingModule.setCustomClientBuilder(
|
|
||||||
new NetworkingModule.CustomClientBuilder() {
|
|
||||||
@Override
|
|
||||||
public void apply(OkHttpClient.Builder builder) {
|
|
||||||
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
client.addPlugin(networkFlipperPlugin);
|
|
||||||
client.start();
|
|
||||||
|
|
||||||
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
|
|
||||||
// Hence we run if after all native modules have been initialized
|
|
||||||
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
|
||||||
if (reactContext == null) {
|
|
||||||
reactInstanceManager.addReactInstanceEventListener(
|
|
||||||
new ReactInstanceEventListener() {
|
|
||||||
@Override
|
|
||||||
public void onReactContextInitialized(ReactContext reactContext) {
|
|
||||||
reactInstanceManager.removeReactInstanceEventListener(this);
|
|
||||||
reactContext.runOnNativeModulesQueueThread(
|
|
||||||
new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
client.addPlugin(new FrescoFlipperPlugin());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
client.addPlugin(new FrescoFlipperPlugin());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:name=".MainApplication"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:allowBackup="false"
|
|
||||||
android:theme="@style/AppTheme">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:windowSoftInputMode="adjustResize"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#FFFFFF</color>
|
|
||||||
</resources>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<resources>
|
|
||||||
<string name="app_name">Öppna Skolplattformen NEW</string>
|
|
||||||
</resources>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<resources>
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
<item name="android:textColor">#000000</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
|
@ -1,20 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
|
||||||
* directory of this source tree.
|
|
||||||
*/
|
|
||||||
package com.oppna_skolplattformen_new.app;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import com.facebook.react.ReactInstanceManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class responsible of loading Flipper inside your React Native application. This is the release
|
|
||||||
* flavor of it so it's empty as we don't want to load Flipper.
|
|
||||||
*/
|
|
||||||
public class ReactNativeFlipper {
|
|
||||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
|
||||||
// Do nothing as we don't want to initialize Flipper on Release.
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext {
|
|
||||||
buildToolsVersion = "33.0.0"
|
|
||||||
minSdkVersion = 21
|
|
||||||
compileSdkVersion = 33
|
|
||||||
targetSdkVersion = 33
|
|
||||||
|
|
||||||
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
|
|
||||||
ndkVersion = "23.1.7779620"
|
|
||||||
}
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath("com.android.tools.build:gradle")
|
|
||||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
# Project-wide Gradle settings.
|
|
||||||
|
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
|
||||||
|
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
|
||||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
|
||||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
|
||||||
|
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
|
||||||
# org.gradle.parallel=true
|
|
||||||
|
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
|
||||||
# Android operating system, and which are packaged with your app's APK
|
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
|
||||||
android.useAndroidX=true
|
|
||||||
# Automatically convert third-party libraries to use AndroidX
|
|
||||||
android.enableJetifier=true
|
|
||||||
|
|
||||||
# Version of flipper SDK to use with React Native
|
|
||||||
FLIPPER_VERSION=0.182.0
|
|
||||||
|
|
||||||
# Use this property to specify which architecture you want to build.
|
|
||||||
# You can also override it from the CLI using
|
|
||||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
|
||||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
|
||||||
|
|
||||||
# Use this property to enable support to the new architecture.
|
|
||||||
# This will allow you to use TurboModules and the Fabric render in
|
|
||||||
# your application. You should enable this flag either if you want
|
|
||||||
# to write custom TurboModules/Fabric components OR use libraries that
|
|
||||||
# are providing them.
|
|
||||||
newArchEnabled=false
|
|
||||||
|
|
||||||
# Use this property to enable or disable the Hermes JS engine.
|
|
||||||
# If set to false, you will be using JSC instead.
|
|
||||||
hermesEnabled=true
|
|
|
@ -1,6 +0,0 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
|
@ -1,244 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015-2021 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# This is normally unused
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
|
||||||
# double quotes to make sure that they get re-expanded; and
|
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
|
@ -1,92 +0,0 @@
|
||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
|
@ -1,4 +0,0 @@
|
||||||
rootProject.name = 'app'
|
|
||||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
|
||||||
include ':app'
|
|
||||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"name": "app",
|
|
||||||
"displayName": "app",
|
|
||||||
"schema": "oppnaskolplattformen://"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 59 KiB |
|
@ -1,37 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Lager_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 595.3 841.9" enable-background="new 0 0 595.3 841.9" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<path fill="#479CBE" d="M241.9,423.6l13.2-83.2c-5.2,0-14.1,0-14.1,0c-6.6,0-15.1-3.7-17.6-10.5c-0.8-2.3-2.7-10.2,8.2-17.9
|
|
||||||
c3.9-2.7,6.4-5.7,6.9-8c0.5-2.4-0.1-4.5-1.8-6.1c-2.4-2.3-7.1-3.6-13.1-3.6c-10.1,0-17.2,5.8-17.9,10c-0.5,3.1,1.9,5.6,4,7.2
|
|
||||||
c6.3,4.7,7.8,11.5,3.9,17.9c-4,6.6-12.7,10.9-22,11c0,0-9.2,0-14.4,0c-1.2,8.1-20.8,132.3-22.3,142.1h78.1
|
|
||||||
c0.7-4.4,4.3-27.9,9.2-58.9H241.9z"/>
|
|
||||||
<path fill="#00A5C3" d="M346.5,267.6H267l-10.6,67.3l13.5,0c7.4,0,14.4-3.4,17.4-8.3c1-1.6,1.4-3,1.4-4.3c0-2.8-1.9-4.9-3.8-6.3
|
|
||||||
c-5.2-3.9-6.3-8-6.3-10.9c0-0.6,0-1.1,0.1-1.6c1.1-7.1,10.7-14.8,23.4-14.8c7.6,0,13.4,1.8,16.9,5.1c3.1,2.9,4.3,7,3.4,11.3
|
|
||||||
c-1.1,5.1-6.2,9.3-9.1,11.4c-7.7,5.4-6.7,10.1-6.2,11.5c1.6,4.2,7.7,6.9,12.4,6.9H340c0,0,0,0,0,0.1c28,0.2,43,13.1,38.3,43.1
|
|
||||||
c-4.4,27.9-25.8,39.9-51.3,40.1l-10.1,64.4h14.9c62.9,0,114.3-40.4,124.4-104.2C468.7,299.2,418.5,267.6,346.5,267.6z"/>
|
|
||||||
<path fill="#235971" d="M346.5,267.6H267l-10.6,67.3l13.5,0c7.4,0,14.4-3.4,17.4-8.3c1-1.6,1.4-3,1.4-4.3c0-2.8-1.9-4.9-3.8-6.3
|
|
||||||
c-5.2-3.9-6.3-8-6.3-10.9c0-0.6,0-1.1,0.1-1.6c1.1-7.1,10.7-14.8,23.4-14.8c7.6,0,13.4,1.8,16.9,5.1c3.1,2.9,4.3,7,3.4,11.3
|
|
||||||
c-1.1,5.1-6.2,9.3-9.1,11.4c-7.7,5.4-6.7,10.1-6.2,11.5c1.6,4.2,7.7,6.9,12.4,6.9H340c0,0,0,0,0,0.1c28,0.2,43,13.1,38.3,43.1
|
|
||||||
c-4.4,27.9-25.8,39.9-51.3,40.1l-10.1,64.4h14.9c62.9,0,114.3-40.4,124.4-104.2C468.7,299.2,418.5,267.6,346.5,267.6z"/>
|
|
||||||
<g>
|
|
||||||
<path fill="#235971" d="M150.7,511.2h31.9c13.6,0,16.9,6.9,15.9,13.2c-0.8,5.1-4.3,8.9-10.3,11.4c7.6,2.9,10.6,7.4,9.5,14.5
|
|
||||||
c-1.4,8.9-9.1,15.5-19.2,15.5h-36.3L150.7,511.2z M171.8,533.8c6.2,0,9.1-3.3,9.7-7.2c0.6-4.2-1.3-7.1-7.5-7.1h-5.5l-2.2,14.3
|
|
||||||
H171.8z M168.4,557.4c6.4,0,10.1-2.6,11-7.9c0.7-4.6-1.9-7.3-8.1-7.3H165l-2.4,15.3H168.4z"/>
|
|
||||||
<path fill="#235971" d="M242.4,566.2c-8.3,0.6-12.3-0.3-14.3-3.9c-4.4,2.7-9.3,4.1-14.5,4.1c-9.4,0-12.7-4.9-11.8-10.3
|
|
||||||
c0.4-2.6,1.9-5.1,4.3-7.2c5.2-4.5,18-5.1,23-8.5c0.4-3.8-1.1-5.2-5.8-5.2c-5.5,0-10.1,1.8-18,7.2l1.9-12.4
|
|
||||||
c6.8-4.9,13.4-7.2,21-7.2c9.7,0,18.3,4,16.7,14.6l-1.9,12c-0.7,4.2-0.5,5.5,4.2,5.6L242.4,566.2z M228,547.4
|
|
||||||
c-4.4,2.8-12.6,2.3-13.5,8.1c-0.4,2.7,1.3,4.7,4,4.7c2.6,0,5.8-1.1,8.4-2.9c-0.2-1-0.1-2,0.2-3.9L228,547.4z"/>
|
|
||||||
<path fill="#235971" d="M257.9,523.5h16.6l-0.9,5.5c5.3-4.5,9.3-6.2,14.5-6.2c9.3,0,13.6,5.7,12.1,15l-4.3,27.9h-16.6l3.6-23.1
|
|
||||||
c0.7-4.2-0.6-6.2-3.8-6.2c-2.6,0-5,1.4-7.3,4.5l-3.8,24.7h-16.6L257.9,523.5z"/>
|
|
||||||
<path fill="#235971" d="M313.1,511.2h16.6l-4.2,26.8l15.9-14.5h20.5l-20.4,18l16.4,24.2h-20.9l-12.6-19.5h-0.2l-3,19.5h-16.6
|
|
||||||
L313.1,511.2z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path fill="#479CBE" d="M371.9,511.2H391l-8.4,54.5h-19.1L371.9,511.2z"/>
|
|
||||||
<path fill="#479CBE" d="M400.3,511.2h27.3c21.1,0,27.2,15.3,25.2,28c-1.9,12.4-11.7,26.5-30.2,26.5h-30.8L400.3,511.2z M418,552.7
|
|
||||||
c9.3,0,14.4-4.6,15.9-14.3c1.1-7.2-1.1-14.3-11.4-14.3h-5.1l-4.4,28.6H418z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 784 KiB |
Before Width: | Height: | Size: 287 KiB |
|
@ -1,93 +0,0 @@
|
||||||
Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
|
|
||||||
|
|
||||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
||||||
This license is copied below, and is also available with a FAQ at:
|
|
||||||
http://scripts.sil.org/OFL
|
|
||||||
|
|
||||||
|
|
||||||
-----------------------------------------------------------
|
|
||||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
||||||
-----------------------------------------------------------
|
|
||||||
|
|
||||||
PREAMBLE
|
|
||||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
||||||
development of collaborative font projects, to support the font creation
|
|
||||||
efforts of academic and linguistic communities, and to provide a free and
|
|
||||||
open framework in which fonts may be shared and improved in partnership
|
|
||||||
with others.
|
|
||||||
|
|
||||||
The OFL allows the licensed fonts to be used, studied, modified and
|
|
||||||
redistributed freely as long as they are not sold by themselves. The
|
|
||||||
fonts, including any derivative works, can be bundled, embedded,
|
|
||||||
redistributed and/or sold with any software provided that any reserved
|
|
||||||
names are not used by derivative works. The fonts and derivatives,
|
|
||||||
however, cannot be released under any other type of license. The
|
|
||||||
requirement for fonts to remain under this license does not apply
|
|
||||||
to any document created using the fonts or their derivatives.
|
|
||||||
|
|
||||||
DEFINITIONS
|
|
||||||
"Font Software" refers to the set of files released by the Copyright
|
|
||||||
Holder(s) under this license and clearly marked as such. This may
|
|
||||||
include source files, build scripts and documentation.
|
|
||||||
|
|
||||||
"Reserved Font Name" refers to any names specified as such after the
|
|
||||||
copyright statement(s).
|
|
||||||
|
|
||||||
"Original Version" refers to the collection of Font Software components as
|
|
||||||
distributed by the Copyright Holder(s).
|
|
||||||
|
|
||||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
||||||
or substituting -- in part or in whole -- any of the components of the
|
|
||||||
Original Version, by changing formats or by porting the Font Software to a
|
|
||||||
new environment.
|
|
||||||
|
|
||||||
"Author" refers to any designer, engineer, programmer, technical
|
|
||||||
writer or other person who contributed to the Font Software.
|
|
||||||
|
|
||||||
PERMISSION & CONDITIONS
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
||||||
redistribute, and sell modified and unmodified copies of the Font
|
|
||||||
Software, subject to the following conditions:
|
|
||||||
|
|
||||||
1) Neither the Font Software nor any of its individual components,
|
|
||||||
in Original or Modified Versions, may be sold by itself.
|
|
||||||
|
|
||||||
2) Original or Modified Versions of the Font Software may be bundled,
|
|
||||||
redistributed and/or sold with any software, provided that each copy
|
|
||||||
contains the above copyright notice and this license. These can be
|
|
||||||
included either as stand-alone text files, human-readable headers or
|
|
||||||
in the appropriate machine-readable metadata fields within text or
|
|
||||||
binary files as long as those fields can be easily viewed by the user.
|
|
||||||
|
|
||||||
3) No Modified Version of the Font Software may use the Reserved Font
|
|
||||||
Name(s) unless explicit written permission is granted by the corresponding
|
|
||||||
Copyright Holder. This restriction only applies to the primary font name as
|
|
||||||
presented to the users.
|
|
||||||
|
|
||||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
||||||
Software shall not be used to promote, endorse or advertise any
|
|
||||||
Modified Version, except to acknowledge the contribution(s) of the
|
|
||||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
||||||
permission.
|
|
||||||
|
|
||||||
5) The Font Software, modified or unmodified, in part or in whole,
|
|
||||||
must be distributed entirely under this license, and must not be
|
|
||||||
distributed under any other license. The requirement for fonts to
|
|
||||||
remain under this license does not apply to any document created
|
|
||||||
using the Font Software.
|
|
||||||
|
|
||||||
TERMINATION
|
|
||||||
This license becomes null and void if any of the above conditions are
|
|
||||||
not met.
|
|
||||||
|
|
||||||
DISCLAIMER
|
|
||||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
||||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
||||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
||||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
||||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Before Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 510 KiB |
Before Width: | Height: | Size: 530 KiB |
|
@ -1,128 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="185px" height="63px" viewBox="0 0 185 63" enable-background="new 0 0 185 63" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path fill="#1D1D1B" d="M52.791,0H0.037C0.02,0.556-0.321,14.721,1.487,23.643c2.958,14.599,11.115,27.729,24.925,39.422
|
|
||||||
c13.813-11.693,21.966-24.824,24.932-39.422C53.154,14.721,52.804,0.556,52.791,0"/>
|
|
||||||
<path fill="#FFFFFF" d="M33.177,9.163c0.631,0,1.146,0.513,1.146,1.144c0,0.629-0.514,1.147-1.146,1.147
|
|
||||||
c-0.63,0-1.144-0.518-1.144-1.147C32.033,9.676,32.546,9.163,33.177,9.163"/>
|
|
||||||
<path fill="#FFFFFF" d="M19.652,9.163c0.63,0,1.142,0.513,1.142,1.144c0,0.629-0.512,1.147-1.142,1.147
|
|
||||||
c-0.631,0-1.147-0.518-1.147-1.147C18.505,9.676,19.021,9.163,19.652,9.163"/>
|
|
||||||
<path fill="#FFFFFF" d="M16.498,39.622c0.978,0,1.025-0.507,1.069-0.669c-0.188,0.188-0.567,0.317-1.154,0.317
|
|
||||||
c-0.593,0-1.166-0.242-1.474-1.201c-0.155-0.496-0.032-1.065-0.032-1.065s0.648,0.517,1.148,0.489
|
|
||||||
c0.889-0.046,1.56-0.612,1.56-1.632c0-1.203-0.856-2.497-2.539-4.494c-1.5-1.776-2.24-3.69-2.24-5.568
|
|
||||||
c0-4.427,4.411-7.22,4.411-7.22s0.461,11.07,0.552,12.266c0.096,1.195,0.375,3.236,1.087,4.821v3.856
|
|
||||||
c-0.195,0.609-0.963,0.863-1.499,0.863C16.968,40.383,16.445,39.989,16.498,39.622"/>
|
|
||||||
<path fill="#FFFFFF" d="M26.413,17.063c-6.53,0-9.876,0.775-9.876,0.775l-0.856-1.723c0,0,4.53-0.87,10.732-0.87
|
|
||||||
c6.202,0,10.739,0.87,10.739,0.87l-0.858,1.723C36.293,17.838,32.947,17.063,26.413,17.063"/>
|
|
||||||
<path fill="#FFFFFF" d="M32.275,48.641H20.556l-1.932-2.165c0,0,1.222-1.114,1.222-3.152v-6.07
|
|
||||||
c-0.021-0.005,2.254,3.292,6.567,3.292c3.789,0,6.61-3.221,6.571-3.21v5.989c0,2.038,1.222,3.152,1.222,3.152L32.275,48.641z"/>
|
|
||||||
<path fill="#FFFFFF" d="M36.775,37.492c0.498,0.028,1.148-0.489,1.148-0.489s0.122,0.569-0.035,1.065
|
|
||||||
c-0.3,0.959-0.881,1.2-1.467,1.2c-0.592,0-0.972-0.127-1.159-0.315c0.044,0.162,0.094,0.669,1.069,0.669
|
|
||||||
c0.053,0.368-0.468,0.762-0.888,0.762c-0.538,0-1.305-0.254-1.498-0.863v-3.856c0.709-1.585,0.986-3.626,1.081-4.821
|
|
||||||
c0.093-1.195,0.559-12.266,0.559-12.266s4.406,2.793,4.406,7.22c0,1.878-0.736,3.792-2.238,5.568
|
|
||||||
c-1.686,1.997-2.539,3.291-2.539,4.494C35.214,36.88,35.881,37.445,36.775,37.492"/>
|
|
||||||
<path fill="#FFFFFF" d="M37.539,15.344c0,0-4.713-0.949-11.126-0.949c-6.41,0-11.12,0.949-11.12,0.949L12.18,9.11
|
|
||||||
c0,0,0.919-0.637,1.576-0.637c1.156,0,1.416,1.039,1.416,1.403c0,0.288-0.035,0.508-0.207,0.705l-0.049,0.05
|
|
||||||
c-0.26,0.295-0.325,0.371-0.325,0.568c0,0.179,0.125,0.46,0.464,0.46c0.299,0,0.371-0.095,0.505-0.472
|
|
||||||
c0.141-0.382,0.516-0.766,1.049-0.766c0.522,0,1.104,0.454,1.104,1.103c0,0.693-0.565,1.107-1.104,1.107
|
|
||||||
c-0.311,0-0.456-0.088-0.557-0.151c-0.069-0.041-0.104-0.062-0.162-0.062c-0.067,0-0.12,0.046-0.148,0.081
|
|
||||||
c-0.086,0.102-0.12,0.271-0.086,0.436c0.088,0.475,0.989,1.211,2,1.211c1.503,0,2.416-1.354,2.416-2.332
|
|
||||||
c0.578,1.375,1.855,1.666,2.74,1.666c2.382,0,2.875-2.113,2.875-2.679c0-0.38-0.122-0.691-0.234-0.746
|
|
||||||
c-0.016-0.004-0.058-0.024-0.134,0.048l-0.097,0.082c-0.288,0.264-0.83,0.751-1.589,0.751c-0.851,0-1.733-0.639-1.733-1.707
|
|
||||||
c0-1.135,0.87-1.728,1.728-1.728c0.714,0,1.117,0.391,1.391,0.652c0.104,0.104,0.213,0.213,0.271,0.213
|
|
||||||
c0.119,0,0.232-0.05,0.306-0.132c0.056-0.065,0.091-0.15,0.091-0.239c0-0.015,0-0.035-0.004-0.052
|
|
||||||
c-0.01-0.077-0.104-0.17-0.213-0.28c-0.234-0.245-0.558-0.579-0.558-1.226c0-0.743,0.603-1.535,1.502-1.539
|
|
||||||
c0.905,0.003,1.505,0.795,1.505,1.539c0,0.647-0.325,0.981-0.562,1.226c-0.106,0.11-0.196,0.203-0.206,0.28
|
|
||||||
c0,0.017-0.003,0.037-0.003,0.052c0,0.089,0.032,0.173,0.089,0.239c0.07,0.083,0.184,0.132,0.305,0.132
|
|
||||||
c0.061,0,0.169-0.108,0.273-0.213c0.269-0.261,0.675-0.652,1.389-0.652c0.857,0,1.727,0.592,1.727,1.728
|
|
||||||
c0,1.067-0.887,1.707-1.735,1.707c-0.755,0-1.3-0.487-1.588-0.751l-0.09-0.082c-0.082-0.072-0.123-0.052-0.137-0.048
|
|
||||||
c-0.11,0.054-0.234,0.366-0.234,0.746c0,0.565,0.49,2.679,2.87,2.679c0.883,0,2.162-0.291,2.74-1.666
|
|
||||||
c0,0.978,0.913,2.332,2.42,2.332c1.011,0,1.908-0.736,1.995-1.211c0.037-0.165,0-0.333-0.085-0.436
|
|
||||||
c-0.03-0.035-0.081-0.081-0.149-0.081c-0.059,0-0.096,0.021-0.161,0.062c-0.102,0.063-0.245,0.151-0.557,0.151
|
|
||||||
c-0.539,0-1.103-0.414-1.103-1.107c0-0.648,0.583-1.103,1.103-1.103c0.531,0,0.909,0.384,1.05,0.766
|
|
||||||
c0.139,0.376,0.208,0.472,0.507,0.472c0.339,0,0.463-0.281,0.463-0.46c0-0.198-0.062-0.273-0.327-0.568l-0.044-0.05
|
|
||||||
c-0.173-0.197-0.212-0.417-0.212-0.705c0-0.364,0.265-1.403,1.42-1.403c0.658,0,1.574,0.637,1.574,0.637L37.539,15.344z"/>
|
|
||||||
<path fill="#FFFFFF" d="M34.523,18.494c-0.04,0.874-0.331,8.023-0.365,10.033c-0.01,0.824-0.233,3.622-0.443,4.671
|
|
||||||
c-0.465,2.318-2.528,6.241-7.303,6.241c-4.769,0-6.833-3.923-7.297-6.241c-0.212-1.053-0.439-3.85-0.449-4.671
|
|
||||||
c-0.027-2.01-0.322-9.159-0.357-10.033c0.521-0.079,3.444-0.488,8.104-0.488C31.079,18.006,34,18.415,34.523,18.494"/>
|
|
||||||
<path fill="#1D1D1B" d="M31.359,21.949c-0.415,0-0.858,0.066-1.219,0.345c-0.191,0.146-0.253,0.347,0.032,0.347
|
|
||||||
c0.498,0,0.812,0.454,0.812,0.794c0,0.34-0.217,0.512-0.467,0.512c-0.251,0-0.391,0.226-0.128,0.28
|
|
||||||
c0.276,0.053,0.552,0.084,0.965,0.084c1.431,0,2.196-0.465,2.196-0.862C33.551,23.051,32.767,21.949,31.359,21.949"/>
|
|
||||||
<path fill="#1D1D1B" d="M21.978,20.422c0.657,0.17,1.63,0.527,2.349,1.249c0.739,0.743,1.024,1.481,1.126,1.994
|
|
||||||
c0.089,0.432,0.243,0.45,0.374,0.029c0.189-0.606,0.246-1.545-0.617-2.514c-0.794-0.892-2.011-1.088-2.727-1.088
|
|
||||||
c-0.174,0-0.346,0.014-0.5,0.036C21.569,20.18,21.548,20.312,21.978,20.422"/>
|
|
||||||
<path fill="#1D1D1B" d="M29.332,33.816c-0.417-0.2-1.565-0.758-1.84-0.883c-0.153-0.073-0.214-0.083-0.375-0.083h-1.419
|
|
||||||
c-0.163,0-0.226,0.01-0.375,0.083c-0.28,0.125-1.43,0.684-1.838,0.883c-0.231,0.11-0.198,0.332,0.023,0.332h5.799
|
|
||||||
C29.527,34.148,29.558,33.926,29.332,33.816"/>
|
|
||||||
<path fill="#1D1D1B" d="M24.168,23.449c0-0.398-0.779-1.502-2.188-1.502c-0.414,0-0.857,0.068-1.217,0.348
|
|
||||||
c-0.19,0.146-0.252,0.347,0.032,0.347c0.495,0,0.812,0.454,0.812,0.794c0,0.34-0.218,0.512-0.47,0.512
|
|
||||||
c-0.246,0-0.389,0.228-0.126,0.28c0.279,0.053,0.552,0.085,0.968,0.085C23.407,24.312,24.168,23.846,24.168,23.449"/>
|
|
||||||
<path fill="#1D1D1B" d="M28.28,34.977h-3.734c-0.196,0-0.226,0.188-0.091,0.278c0.117,0.083,0.884,0.593,1.057,0.702
|
|
||||||
c0.135,0.083,0.2,0.095,0.365,0.095h1.071c0.167,0,0.234-0.013,0.369-0.095c0.17-0.109,0.936-0.619,1.055-0.702
|
|
||||||
C28.502,35.164,28.477,34.977,28.28,34.977"/>
|
|
||||||
<path fill="#1D1D1B" d="M32.222,20.096c-0.551,0-0.905-0.003-1.822-0.003c-1.886,0-3.193,1.564-3.193,3.462v5.552
|
|
||||||
c0,0.316-0.122,0.511-0.454,0.511h-0.337c-0.182,0-0.373,0.044-0.546,0.191c-0.284,0.238-0.975,0.833-1.068,0.953
|
|
||||||
c-0.1,0.112-0.072,0.25,0.215,0.25h3.73c0.212,0,0.304-0.093,0.13-0.25c-0.234-0.217-0.988-0.941-0.988-0.941v-5.599
|
|
||||||
c0-1.459,0.282-3.255,3.032-3.255c0.731,0,1.379,0.002,1.497,0.002c1.003,0,1.537-0.876,1.537-0.876S32.769,20.096,32.222,20.096"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path fill="#1D1D1B" d="M62.277,21.176c0.812,0.977,2.188,1.797,3.627,1.866c1.634,0.079,2.689-0.838,2.689-1.813
|
|
||||||
c0-1.18-0.886-1.871-2.548-2.562l-1.328-0.536c-2.537-1.046-3.793-2.562-3.793-4.613c0-2.538,2.441-4.187,5.004-4.187
|
|
||||||
c1.945,0,3.428,0.645,4.661,2.026l-1.844,1.645c-0.84-0.905-1.646-1.266-2.812-1.266c-1.138,0-2.324,0.637-2.324,1.731
|
|
||||||
c0,1.024,0.823,1.79,2.507,2.46l1.307,0.557c2.299,0.934,3.867,2.316,3.867,4.522c0,2.209-1.772,4.501-5.362,4.501
|
|
||||||
c-2.479,0-4.289-1.123-5.708-2.566L62.277,21.176z"/>
|
|
||||||
<path fill="#1D1D1B" d="M79.538,25.309H77.5c-2.387,0-3.801-1.416-3.801-3.776v-6.21h-2.025v-2.434h2.025V9.963h2.73v2.925h3.108
|
|
||||||
v2.434H76.43v5.64c0,1.266,0.348,1.838,1.765,1.838h1.344V25.309z"/>
|
|
||||||
<path fill="#1D1D1B" d="M80.835,19.099c0-3.626,2.732-6.409,6.435-6.409c3.675,0,6.432,2.782,6.432,6.409
|
|
||||||
c0,3.627-2.757,6.408-6.432,6.408C83.567,25.507,80.835,22.726,80.835,19.099 M90.895,19.099c0-2.161-1.515-3.8-3.625-3.8
|
|
||||||
c-2.138,0-3.626,1.64-3.626,3.8c0,2.161,1.489,3.801,3.626,3.801C89.38,22.9,90.895,21.26,90.895,19.099"/>
|
|
||||||
<g>
|
|
||||||
<path fill="#1D1D1B" d="M95.46,19.099c0-3.626,2.731-6.409,6.409-6.409c1.985,0,3.6,0.821,4.744,2.063l-1.864,1.912
|
|
||||||
c-0.645-0.746-1.589-1.318-2.88-1.318c-2.113,0-3.603,1.591-3.603,3.751c0,2.161,1.49,3.751,3.603,3.751
|
|
||||||
c1.291,0,2.235-0.57,2.88-1.317l1.864,1.913c-1.144,1.241-2.759,2.062-4.744,2.062C98.191,25.507,95.46,22.726,95.46,19.099"/>
|
|
||||||
</g>
|
|
||||||
<polygon fill="#1D1D1B" points="111.33,25.309 108.598,25.309 108.598,7.969 111.33,7.969 111.33,18.503 116.297,12.889
|
|
||||||
119.527,12.889 114.362,18.776 120.147,25.309 116.796,25.309 111.33,19.099 "/>
|
|
||||||
<path fill="#1D1D1B" d="M124.18,17.485v7.825h-2.732V7.969h2.732v6.088c0.869-0.945,2.185-1.367,3.479-1.367
|
|
||||||
c3.079,0,5.042,2.137,5.042,5.341v7.278h-2.735V18.08c0-1.341-0.991-2.782-2.83-2.782
|
|
||||||
C125.721,15.298,124.652,16.043,124.18,17.485"/>
|
|
||||||
<g>
|
|
||||||
<path fill="#1D1D1B" d="M134.916,19.099c0-3.626,2.73-6.409,6.432-6.409c3.676,0,6.433,2.782,6.433,6.409
|
|
||||||
c0,3.627-2.757,6.408-6.433,6.408C137.646,25.507,134.916,22.726,134.916,19.099 M144.974,19.099c0-2.161-1.515-3.8-3.626-3.8
|
|
||||||
c-2.137,0-3.626,1.64-3.626,3.8c0,2.161,1.49,3.801,3.626,3.801C143.459,22.9,144.974,21.26,144.974,19.099"/>
|
|
||||||
</g>
|
|
||||||
<rect x="149.996" y="7.969" fill="#1D1D1B" width="2.732" height="17.34"/>
|
|
||||||
<path fill="#1D1D1B" d="M158.134,17.409v7.9h-2.734v-12.42h2.734v1.168c0.844-0.97,2.061-1.366,3.206-1.366
|
|
||||||
c1.737,0,3.053,0.694,3.872,1.912c1.168-1.541,2.908-1.912,4.099-1.912c3.057,0,4.794,2.135,4.794,5.34v7.277h-2.732V18.08
|
|
||||||
c0-1.341-0.771-2.782-2.558-2.782c-1.243,0-2.236,0.745-2.708,2.162c0.026,0.198,0.026,0.373,0.026,0.571v7.277h-2.733V18.08
|
|
||||||
c0-1.341-0.794-2.782-2.582-2.782C159.6,15.298,158.58,16.043,158.134,17.409"/>
|
|
||||||
<path fill="#1D1D1B" d="M175.632,23.354l0.221,0.201c1.478,1.343,2.901,1.996,4.649,1.996c2.954,0,4.498-1.856,4.498-3.689
|
|
||||||
c0-1.631-1.051-2.835-3.213-3.684l-1.035-0.427c-1.619-0.621-1.805-1.225-1.805-1.629c0-0.657,0.792-1.102,1.558-1.102
|
|
||||||
c0.844,0,1.41,0.297,2.017,0.928l0.18,0.189l1.827-1.576l-0.184-0.197c-1.003-1.086-2.21-1.653-3.844-1.653
|
|
||||||
c-2.074,0-4.22,1.292-4.22,3.453c0,1.655,1.064,2.917,3.163,3.754l1.043,0.404c1.341,0.54,1.837,1.002,1.837,1.709
|
|
||||||
c0,0.549-0.631,1.12-1.69,1.12c-0.047,0-0.093-0.001-0.141-0.003c-0.961-0.044-2.079-0.514-2.734-1.273l-0.178-0.207
|
|
||||||
L175.632,23.354z"/>
|
|
||||||
<path fill="#1D1D1B" d="M61.109,43.729l0.219,0.2c1.479,1.346,2.902,1.997,4.65,1.997c2.952,0,4.497-1.855,4.497-3.687
|
|
||||||
c0-1.632-1.05-2.836-3.212-3.686l-1.038-0.427c-1.618-0.619-1.804-1.225-1.804-1.627c0-0.66,0.793-1.103,1.561-1.103
|
|
||||||
c0.844,0,1.407,0.296,2.015,0.928l0.18,0.188l1.828-1.575l-0.185-0.197c-1.002-1.086-2.21-1.654-3.843-1.654
|
|
||||||
c-2.074,0-4.219,1.292-4.219,3.452c0,1.656,1.064,2.919,3.162,3.757l1.042,0.404c1.341,0.539,1.84,1.001,1.84,1.707
|
|
||||||
c0,0.55-0.633,1.121-1.693,1.121c-0.045,0-0.093-0.001-0.141-0.004c-0.961-0.045-2.077-0.512-2.733-1.273l-0.177-0.208
|
|
||||||
L61.109,43.729z"/>
|
|
||||||
<path fill="#1D1D1B" d="M79.538,45.726H77.5c-2.387,0-3.801-1.416-3.801-3.774v-6.21h-2.025v-2.434h2.025v-2.926h2.73v2.926h3.108
|
|
||||||
v2.434H76.43v5.638c0,1.268,0.348,1.839,1.765,1.839h1.344V45.726z"/>
|
|
||||||
<path fill="#1D1D1B" d="M90.547,44.809c-0.868,0.694-2.01,1.116-3.327,1.116c-3.676,0-6.385-2.781-6.385-6.408
|
|
||||||
c0-3.625,2.709-6.407,6.385-6.407c1.317,0,2.458,0.422,3.327,1.117v-0.917h2.733v12.417h-2.733V44.809z M90.547,41.628v-4.222
|
|
||||||
c-0.646-1.018-1.912-1.689-3.302-1.689c-2.112,0-3.601,1.639-3.601,3.8c0,2.16,1.489,3.801,3.601,3.801
|
|
||||||
C88.635,43.317,89.901,42.647,90.547,41.628"/>
|
|
||||||
<path fill="#1D1D1B" d="M105.171,44.809c-0.868,0.694-2.01,1.116-3.327,1.116c-3.677,0-6.384-2.781-6.384-6.408
|
|
||||||
c0-3.625,2.707-6.407,6.384-6.407c1.316,0,2.458,0.422,3.327,1.117v-5.839h2.733v17.338h-2.733V44.809z M105.171,41.628v-4.222
|
|
||||||
c-0.646-1.018-1.912-1.689-3.302-1.689c-2.113,0-3.603,1.639-3.603,3.8c0,2.16,1.49,3.801,3.603,3.801
|
|
||||||
C103.259,43.317,104.524,42.647,105.171,41.628"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 537 KiB |
|
@ -1,107 +0,0 @@
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
||||||
import { useRoute } from '@react-navigation/native'
|
|
||||||
import { useUser } from '../../libs/hooks/src'
|
|
||||||
import { fireEvent, waitFor } from '@testing-library/react-native'
|
|
||||||
import Mockdate from 'mockdate'
|
|
||||||
import React from 'react'
|
|
||||||
import { useSMS } from '../../utils/SMS'
|
|
||||||
import { render } from '../../utils/testHelpers'
|
|
||||||
import Absence from '../absence.component'
|
|
||||||
|
|
||||||
let sendSMS
|
|
||||||
let user = { personalNumber: '201701092395' }
|
|
||||||
|
|
||||||
jest.mock('../../utils/SMS')
|
|
||||||
jest.mock('../../libs/hooks/src')
|
|
||||||
|
|
||||||
const setup = (customProps = {}) => {
|
|
||||||
sendSMS = jest.fn()
|
|
||||||
|
|
||||||
useSMS.mockReturnValue({ sendSMS })
|
|
||||||
useRoute.mockReturnValue({ params: { child: { id: '1' } } })
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
...customProps,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(<Absence {...props} />)
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
// Hide errors from act
|
|
||||||
// https://github.com/callstack/react-native-testing-library/issues/379
|
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {
|
|
||||||
// noop
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
useUser.mockReturnValue({
|
|
||||||
data: user,
|
|
||||||
status: 'loaded',
|
|
||||||
})
|
|
||||||
await AsyncStorage.clear()
|
|
||||||
})
|
|
||||||
|
|
||||||
test.skip('can fill out the form with full day absence', async () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
fireEvent.changeText(
|
|
||||||
screen.getByTestId('personalIdentityNumberInput'),
|
|
||||||
'1212121212'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await waitFor(() => fireEvent.press(screen.getByText('Skicka')))
|
|
||||||
|
|
||||||
expect(screen.queryByText(/starttid/i)).toBeFalsy()
|
|
||||||
expect(screen.queryByText(/sluttid/i)).toBeFalsy()
|
|
||||||
|
|
||||||
expect(sendSMS).toHaveBeenCalledWith('121212-1212')
|
|
||||||
})
|
|
||||||
|
|
||||||
test.skip('handles missing social security number', async () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
await waitFor(() => fireEvent.press(screen.getByText('Skicka')))
|
|
||||||
|
|
||||||
expect(screen.getByText(/Personnummer saknas/i)).toBeTruthy()
|
|
||||||
expect(sendSMS).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test.skip('validates social security number', async () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
fireEvent.changeText(
|
|
||||||
screen.getByTestId('personalIdentityNumberInput'),
|
|
||||||
'12121212'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await waitFor(() => fireEvent.press(screen.getByText('Skicka')))
|
|
||||||
|
|
||||||
expect(screen.getByText(/Personnumret är ogiltigt/i)).toBeTruthy()
|
|
||||||
expect(sendSMS).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test.skip('can fill out the form with part of day absence', async () => {
|
|
||||||
Mockdate.set('2021-02-18 15:30')
|
|
||||||
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
fireEvent.changeText(
|
|
||||||
screen.getByTestId('personalIdentityNumberInput'),
|
|
||||||
'1212121212'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await waitFor(() => fireEvent.press(screen.getByText('Heldag')))
|
|
||||||
|
|
||||||
expect(screen.getByText(/starttid/i)).toBeTruthy()
|
|
||||||
expect(screen.getByText(/sluttid/i)).toBeTruthy()
|
|
||||||
|
|
||||||
await waitFor(() => fireEvent.press(screen.getByText('Skicka')))
|
|
||||||
|
|
||||||
expect(sendSMS).toHaveBeenCalledWith('121212-1212 1500-1700')
|
|
||||||
})
|
|
|
@ -1,239 +0,0 @@
|
||||||
import 'setImmediate'
|
|
||||||
import { useNavigation } from '@react-navigation/core'
|
|
||||||
import {
|
|
||||||
useApi,
|
|
||||||
useCalendar,
|
|
||||||
useChildList,
|
|
||||||
useClassmates,
|
|
||||||
useMenu,
|
|
||||||
useNews,
|
|
||||||
useNotifications,
|
|
||||||
useSchedule,
|
|
||||||
useTimetable,
|
|
||||||
} from '../../libs/hooks/src'
|
|
||||||
import React from 'react'
|
|
||||||
import * as RNLocalize from 'react-native-localize'
|
|
||||||
import { render } from '../../utils/testHelpers'
|
|
||||||
import { translate } from '../../utils/translation'
|
|
||||||
import { Children } from '../children.component'
|
|
||||||
|
|
||||||
jest.mock('../../libs/hooks/src')
|
|
||||||
|
|
||||||
const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))
|
|
||||||
|
|
||||||
const setup = () => {
|
|
||||||
return render(<Children />)
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
useApi.mockReturnValue({
|
|
||||||
api: { on: jest.fn(), off: jest.fn(), logout: jest.fn() },
|
|
||||||
isLoggedIn: false,
|
|
||||||
})
|
|
||||||
RNLocalize.findBestAvailableLanguage.mockImplementationOnce(() => ({
|
|
||||||
languageTag: 'sv',
|
|
||||||
isRTL: false,
|
|
||||||
}))
|
|
||||||
useCalendar.mockReturnValueOnce({ data: [], status: 'loaded' })
|
|
||||||
useNotifications.mockReturnValueOnce({ data: [], status: 'loaded' })
|
|
||||||
useNews.mockReturnValueOnce({ data: [], status: 'loaded' })
|
|
||||||
useSchedule.mockReturnValueOnce({ data: [], status: 'loaded' })
|
|
||||||
useMenu.mockReturnValueOnce({ data: [], status: 'loaded' })
|
|
||||||
useTimetable.mockReturnValueOnce({ data: [], status: 'loaded' })
|
|
||||||
useClassmates.mockReturnValueOnce({ data: [], status: 'loaded' })
|
|
||||||
useNavigation.mockReturnValue({ navigate: jest.fn(), setOptions: jest.fn() })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders loading state', async () => {
|
|
||||||
useChildList.mockImplementationOnce(() => ({
|
|
||||||
data: [],
|
|
||||||
status: 'loading',
|
|
||||||
}))
|
|
||||||
|
|
||||||
const screen = setup()
|
|
||||||
expect(screen.getByText(translate('general.loading'))).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders empty state message', () => {
|
|
||||||
useChildList.mockImplementationOnce(() => ({
|
|
||||||
data: [],
|
|
||||||
status: 'loaded',
|
|
||||||
}))
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByText(translate('children.noKids_description'))
|
|
||||||
).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders error state message', () => {
|
|
||||||
useChildList.mockImplementationOnce(() => ({
|
|
||||||
data: [],
|
|
||||||
status: 'error',
|
|
||||||
}))
|
|
||||||
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByText(translate('children.loadingErrorHeading'))
|
|
||||||
).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders child in preschool', () => {
|
|
||||||
useChildList.mockImplementationOnce(() => ({
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: 'Test Testsson',
|
|
||||||
status: 'F',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
status: 'loaded',
|
|
||||||
}))
|
|
||||||
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText('Test Testsson')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders child in elementary school', () => {
|
|
||||||
useChildList.mockImplementationOnce(() => ({
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: 'Test Testsson',
|
|
||||||
status: 'GR',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
status: 'loaded',
|
|
||||||
}))
|
|
||||||
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText('Test Testsson')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders child in high school', () => {
|
|
||||||
useChildList.mockImplementationOnce(() => ({
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: 'Test Testsson',
|
|
||||||
status: 'G',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
status: 'loaded',
|
|
||||||
}))
|
|
||||||
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText('Test Testsson')).toBeTruthy()
|
|
||||||
expect(
|
|
||||||
screen.getByText(translate('abbrevations.upperSecondarySchool'))
|
|
||||||
).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders multiple children', () => {
|
|
||||||
useChildList.mockImplementationOnce(() => ({
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: 'Storasyster Testsson',
|
|
||||||
status: 'G',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Lillebror Testsson',
|
|
||||||
status: 'GR',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
status: 'loaded',
|
|
||||||
}))
|
|
||||||
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText('Storasyster Testsson')).toBeTruthy()
|
|
||||||
expect(
|
|
||||||
screen.getByText(translate('abbrevations.upperSecondarySchool'))
|
|
||||||
).toBeTruthy()
|
|
||||||
|
|
||||||
expect(screen.getByText('Lillebror Testsson')).toBeTruthy()
|
|
||||||
expect(
|
|
||||||
screen.getByText(translate('abbrevations.compulsorySchool'))
|
|
||||||
).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders child in class', () => {
|
|
||||||
useChildList.mockImplementationOnce(() => ({
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: 'Test Testsson',
|
|
||||||
status: 'G',
|
|
||||||
schoolID: 'Vallaskolan',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
status: 'loaded',
|
|
||||||
}))
|
|
||||||
useClassmates.mockReset()
|
|
||||||
useClassmates.mockImplementationOnce(() => ({
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
className: '8C',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
status: 'loaded',
|
|
||||||
}))
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText('Test Testsson')).toBeTruthy()
|
|
||||||
expect(screen.getByText('8C • Vallaskolan')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('removes any parenthesis from name', () => {
|
|
||||||
useChildList.mockImplementationOnce(() => ({
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: 'Test Testsson (elev)',
|
|
||||||
status: 'G',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
status: 'loaded',
|
|
||||||
}))
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText('Test Testsson')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('handles multiple statuses for a child', () => {
|
|
||||||
useChildList.mockImplementationOnce(() => ({
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: 'Test Testsson(elev)',
|
|
||||||
status: 'G;GR;F',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
status: 'loaded',
|
|
||||||
}))
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
var multipleStatusesRendered = `${translate(
|
|
||||||
'abbrevations.upperSecondarySchool'
|
|
||||||
)}, ${translate('abbrevations.compulsorySchool')}, ${translate(
|
|
||||||
'abbrevations.leisureTimeCentre'
|
|
||||||
)}`
|
|
||||||
|
|
||||||
expect(screen.getByText('Test Testsson')).toBeTruthy()
|
|
||||||
expect(screen.getByText(multipleStatusesRendered)).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('says if there is nothing new this week', () => {
|
|
||||||
useChildList.mockImplementationOnce(() => ({
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: 'Kanye West',
|
|
||||||
status: 'F',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
status: 'loaded',
|
|
||||||
}))
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByText(translate('news.noNewNewsItemsThisWeek'))
|
|
||||||
).toBeTruthy()
|
|
||||||
})
|
|
|
@ -1,86 +0,0 @@
|
||||||
import { useClassmates } from '../../libs/hooks/src'
|
|
||||||
import React from 'react'
|
|
||||||
import { render } from '../../utils/testHelpers'
|
|
||||||
import { ChildProvider } from '../childContext.component'
|
|
||||||
import { Classmates } from '../classmates.component'
|
|
||||||
|
|
||||||
jest.mock('../../libs/hooks/src')
|
|
||||||
|
|
||||||
const defaultClassmates = [
|
|
||||||
{
|
|
||||||
className: '2B',
|
|
||||||
firstname: 'Tyrell',
|
|
||||||
lastname: 'Eriksson',
|
|
||||||
guardians: [
|
|
||||||
{
|
|
||||||
firstname: 'Margaery',
|
|
||||||
lastname: 'Eriksson',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
firstname: 'Loras',
|
|
||||||
lastname: 'Eriksson',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
className: '2B',
|
|
||||||
firstname: 'Adam',
|
|
||||||
lastname: 'Svensson',
|
|
||||||
guardians: [
|
|
||||||
{
|
|
||||||
firstname: 'Eva',
|
|
||||||
lastname: 'Svensson',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const setup = ({ classmates } = { classmates: defaultClassmates }) => {
|
|
||||||
useClassmates.mockReturnValue({
|
|
||||||
data: classmates,
|
|
||||||
})
|
|
||||||
|
|
||||||
return render(
|
|
||||||
<ChildProvider child={{ id: 1 }}>
|
|
||||||
<Classmates />
|
|
||||||
</ChildProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
test('gets the classmates for a child from context', () => {
|
|
||||||
setup()
|
|
||||||
|
|
||||||
expect(useClassmates).toHaveBeenCalledWith({ id: 1 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders class name', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText(/^klass 2b$/i)).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders class without name', () => {
|
|
||||||
const screen = setup({
|
|
||||||
classmates: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(screen.getByText(/^klass$/i)).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders classmates sorted by first name', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByLabelText('Barn 1')).toContainElement(
|
|
||||||
screen.getByText(/adam svensson/i)
|
|
||||||
)
|
|
||||||
expect(screen.getByLabelText('Barn 2')).toContainElement(
|
|
||||||
screen.getByText(/tyrell eriksson/i)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders guardians sorted by first name', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText(/eva svensson/i)).toBeTruthy()
|
|
||||||
expect(screen.getByText(/^loras eriksson, margaery eriksson$/i)).toBeTruthy()
|
|
||||||
})
|
|
|
@ -1,137 +0,0 @@
|
||||||
import { fireEvent } from '@testing-library/react-native'
|
|
||||||
import React from 'react'
|
|
||||||
import { Linking } from 'react-native'
|
|
||||||
import { render } from '../../utils/testHelpers'
|
|
||||||
import { ContactMenu } from '../contactMenu.component'
|
|
||||||
import { act } from 'react-test-renderer'
|
|
||||||
|
|
||||||
const defaultGuardian = {
|
|
||||||
address: 'Testgatan',
|
|
||||||
email: 'adam@adamsson.se',
|
|
||||||
firstname: 'Adam',
|
|
||||||
lastname: 'Adamsson',
|
|
||||||
mobile: '0701234567',
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
contact: {
|
|
||||||
guardians: [defaultGuardian],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const setup = (customProps = {}) => {
|
|
||||||
const props = {
|
|
||||||
...defaultProps,
|
|
||||||
...customProps,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(<ContactMenu {...props} />)
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
// Hide errors from state illegal state transition
|
|
||||||
// Probably due to mock
|
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {
|
|
||||||
// noop
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(jest.clearAllMocks)
|
|
||||||
|
|
||||||
test('renders a parent', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('ShowContactInfoButton'))
|
|
||||||
|
|
||||||
expect(screen.getByText(/adam adamsson/i)).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('displays option to call and text guardian', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('ShowContactInfoButton'))
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByText(/ring/i))
|
|
||||||
expect(Linking.openURL).toHaveBeenCalledWith('tel:0701234567')
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByText(/sms/i))
|
|
||||||
expect(Linking.openURL).toHaveBeenCalledWith('sms:0701234567')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('hides options to call and text if no phone number', () => {
|
|
||||||
const guardianWithoutPhoneNumber = {
|
|
||||||
contact: {
|
|
||||||
guardians: [
|
|
||||||
{
|
|
||||||
...defaultGuardian,
|
|
||||||
mobile: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const screen = setup(guardianWithoutPhoneNumber)
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('ShowContactInfoButton'))
|
|
||||||
|
|
||||||
expect(screen.queryByTestId('CallMenuItem')).toBeNull()
|
|
||||||
expect(screen.queryByTestId('SMSMenuItem')).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('displays option to email guardian', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('ShowContactInfoButton'))
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByText(/maila/i))
|
|
||||||
expect(Linking.openURL).toHaveBeenCalledWith('mailto:adam@adamsson.se')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('hides options to email phone number', () => {
|
|
||||||
const guardianWithoutEmail = {
|
|
||||||
contact: {
|
|
||||||
guardians: [
|
|
||||||
{
|
|
||||||
...defaultGuardian,
|
|
||||||
email: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const screen = setup(guardianWithoutEmail)
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('ShowContactInfoButton'))
|
|
||||||
|
|
||||||
expect(screen.queryByTestId('SendEmailMenuItem')).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('displays address of guardian', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('ShowContactInfoButton'))
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByText(/adress/i))
|
|
||||||
expect(Linking.openURL).toHaveBeenCalledWith(
|
|
||||||
'http://maps.apple.com/?daddr=Testgatan'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('hides address if it does not exist', () => {
|
|
||||||
const guardianWithoutAddress = {
|
|
||||||
contact: {
|
|
||||||
guardians: [
|
|
||||||
{
|
|
||||||
...defaultGuardian,
|
|
||||||
address: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const screen = setup(guardianWithoutAddress)
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('ShowContactInfoButton'))
|
|
||||||
|
|
||||||
expect(screen.queryByTestId('ShowHomeMenuItem')).toBeNull()
|
|
||||||
})
|
|
|
@ -1,50 +0,0 @@
|
||||||
import { useMenu } from '../../libs/hooks/src'
|
|
||||||
import React from 'react'
|
|
||||||
import { render } from '../../utils/testHelpers'
|
|
||||||
import { translate } from '../../utils/translation'
|
|
||||||
import { Menu } from '../menu.component'
|
|
||||||
|
|
||||||
jest.mock('../../libs/hooks/src')
|
|
||||||
|
|
||||||
const defaultItemList = [
|
|
||||||
{
|
|
||||||
title: 'Måndag vecka 10',
|
|
||||||
description: 'Krämiga köttbullar',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Tisdag vecka 10',
|
|
||||||
description: 'Kryddig falukorv',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Onsdag vecka 10',
|
|
||||||
description: 'Sushi',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const setup = (itemList = defaultItemList) => {
|
|
||||||
useMenu.mockReturnValue({
|
|
||||||
data: itemList,
|
|
||||||
})
|
|
||||||
|
|
||||||
return render(<Menu />)
|
|
||||||
}
|
|
||||||
|
|
||||||
test('renders multiple days', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText('Måndag vecka 10')).toBeTruthy()
|
|
||||||
expect(screen.getByText('Tisdag vecka 10')).toBeTruthy()
|
|
||||||
expect(screen.getByText('Onsdag vecka 10')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders title and description', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText('Måndag vecka 10')).toBeTruthy()
|
|
||||||
expect(screen.getByText('Krämiga köttbullar')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders empty menu', () => {
|
|
||||||
const screen = setup([])
|
|
||||||
expect(screen.getByText(translate('menu.emptyText'))).toBeTruthy()
|
|
||||||
})
|
|
|
@ -1,86 +0,0 @@
|
||||||
import { useApi, useNewsDetails } from '../../libs/hooks/src'
|
|
||||||
import React from 'react'
|
|
||||||
import { render } from '../../utils/testHelpers'
|
|
||||||
import { NewsItem } from '../newsItem.component'
|
|
||||||
|
|
||||||
jest.mock('../../libs/hooks/src')
|
|
||||||
|
|
||||||
const defaultNewsItem = {
|
|
||||||
author: 'Köket',
|
|
||||||
fullImageUrl: 'test.png',
|
|
||||||
header: 'K-bullar!',
|
|
||||||
published: '2021-02-15T09:13:28.484Z',
|
|
||||||
modified: '2021-02-15T09:13:28.484Z',
|
|
||||||
}
|
|
||||||
|
|
||||||
let navigation
|
|
||||||
|
|
||||||
const setup = (customProps = { newsItem: {} }) => {
|
|
||||||
useApi.mockReturnValue({ api: { getSessionCookie: jest.fn() } })
|
|
||||||
|
|
||||||
useNewsDetails.mockReturnValue({
|
|
||||||
data: {
|
|
||||||
body: 'Nu blir det köttbullar',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
navigation = {
|
|
||||||
goBack: jest.fn(),
|
|
||||||
}
|
|
||||||
|
|
||||||
const newsItem = {
|
|
||||||
...defaultNewsItem,
|
|
||||||
...customProps.newsItem,
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
navigation,
|
|
||||||
route: {
|
|
||||||
params: {
|
|
||||||
child: { id: 1 },
|
|
||||||
newsItem,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...customProps,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(<NewsItem {...props} />)
|
|
||||||
}
|
|
||||||
|
|
||||||
test('gets article details using useNewsDetails', async () => {
|
|
||||||
setup()
|
|
||||||
|
|
||||||
expect(useNewsDetails).toHaveBeenCalledWith({ id: 1 }, defaultNewsItem)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders an article', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText(/nu blir det köttbullar/i)).toBeTruthy()
|
|
||||||
expect(screen.getByText('Publicerad: 15 feb 2021 10:13')).toBeTruthy()
|
|
||||||
expect(screen.getByText('Uppdaterad: 15 feb 2021 10:13')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders an article without published date if date is invalid', () => {
|
|
||||||
const newsItemWithoutPublishedDate = {
|
|
||||||
...defaultNewsItem,
|
|
||||||
published: '2020-08-16T21:10:00.000+02:0',
|
|
||||||
}
|
|
||||||
const screen = setup({ newsItem: newsItemWithoutPublishedDate })
|
|
||||||
|
|
||||||
expect(screen.getByText(/nu blir det köttbullar/i)).toBeTruthy()
|
|
||||||
expect(screen.getByText('Uppdaterad: 15 feb 2021 10:13')).toBeTruthy()
|
|
||||||
expect(screen.queryByText('Publicerad: Invalid DateTime')).toBeFalsy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders an article without modified date if date is invalid', () => {
|
|
||||||
const newsItemWithoutPublishedDate = {
|
|
||||||
...defaultNewsItem,
|
|
||||||
modified: null,
|
|
||||||
}
|
|
||||||
const screen = setup({ newsItem: newsItemWithoutPublishedDate })
|
|
||||||
|
|
||||||
expect(screen.getByText(/nu blir det köttbullar/i)).toBeTruthy()
|
|
||||||
expect(screen.getByText('Publicerad: 15 feb 2021 10:13')).toBeTruthy()
|
|
||||||
expect(screen.queryByText('Uppdaterad: Invalid DateTime')).toBeFalsy()
|
|
||||||
})
|
|
|
@ -1,81 +0,0 @@
|
||||||
import { useNavigation } from '@react-navigation/native'
|
|
||||||
import { fireEvent } from '@testing-library/react-native'
|
|
||||||
import MockDate from 'mockdate'
|
|
||||||
import React from 'react'
|
|
||||||
import { render } from '../../utils/testHelpers'
|
|
||||||
import { ChildProvider } from '../childContext.component'
|
|
||||||
import { NewsListItem } from '../newsListItem.component'
|
|
||||||
|
|
||||||
const defaultItem = {
|
|
||||||
author: 'Köket',
|
|
||||||
intro: 'Nu blir det köttbullar',
|
|
||||||
header: 'K-bullar!',
|
|
||||||
published: '2021-02-15T09:13:28.484Z',
|
|
||||||
modified: '2021-02-15T09:13:28.484Z',
|
|
||||||
}
|
|
||||||
|
|
||||||
const setup = (customProps = {}) => {
|
|
||||||
const props = {
|
|
||||||
item: defaultItem,
|
|
||||||
...customProps,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(
|
|
||||||
<ChildProvider child={{ id: 1 }}>
|
|
||||||
<NewsListItem {...props} />
|
|
||||||
</ChildProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
MockDate.set('2021-02-15T09:30:28.484Z')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders an article', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText(/k-bullar!/i)).toBeTruthy()
|
|
||||||
expect(screen.getByText(/nu blir det köttbullar/i)).toBeTruthy()
|
|
||||||
expect(screen.getByText('Köket • för 17 minuter sedan')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders article without date', () => {
|
|
||||||
const itemWithInvalidDate = {
|
|
||||||
...defaultItem,
|
|
||||||
published: null,
|
|
||||||
modified: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
const screen = setup({ item: itemWithInvalidDate })
|
|
||||||
|
|
||||||
expect(screen.getByText(/k-bullar!/i)).toBeTruthy()
|
|
||||||
expect(screen.getByText(/nu blir det köttbullar/i)).toBeTruthy()
|
|
||||||
expect(screen.getByText(/^köket$/i)).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('falls back to modified date if no published date', () => {
|
|
||||||
const itemWithInvalidDate = {
|
|
||||||
...defaultItem,
|
|
||||||
published: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
const screen = setup({ item: itemWithInvalidDate })
|
|
||||||
|
|
||||||
expect(screen.getByText(/k-bullar!/i)).toBeTruthy()
|
|
||||||
expect(screen.getByText(/nu blir det köttbullar/i)).toBeTruthy()
|
|
||||||
expect(screen.getByText('Köket • för 17 minuter sedan')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('navigates to news article on press', () => {
|
|
||||||
const navigate = jest.fn()
|
|
||||||
useNavigation.mockReturnValue({ navigate })
|
|
||||||
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByText(/k-bullar!/i))
|
|
||||||
|
|
||||||
expect(navigate).toHaveBeenCalledWith('NewsItem', {
|
|
||||||
child: { id: 1 },
|
|
||||||
newsItem: defaultItem,
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,73 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { render } from '../../utils/testHelpers'
|
|
||||||
import { Notification } from '../notification.component'
|
|
||||||
import MockDate from 'mockdate'
|
|
||||||
|
|
||||||
const defaultItem = {
|
|
||||||
sender: 'Planering',
|
|
||||||
category: 'Bedömning',
|
|
||||||
dateCreated: '2021-02-15T09:13:28.484Z',
|
|
||||||
dateModified: '2021-02-15T09:14:28.484Z',
|
|
||||||
}
|
|
||||||
|
|
||||||
// copied from https://github.com/react-native-webview/react-native-webview/issues/2934#issuecomment-1524101977
|
|
||||||
jest.mock('react-native-webview', () => {
|
|
||||||
const { View } = require('react-native')
|
|
||||||
return {
|
|
||||||
WebView: View,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
//
|
|
||||||
|
|
||||||
const setup = (customProps = {}) => {
|
|
||||||
const props = {
|
|
||||||
item: defaultItem,
|
|
||||||
...customProps,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(<Notification {...props} />)
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
MockDate.set('2021-02-15T09:30:28.484Z')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders subtitle with modified date', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
expect(screen.getByText('Bedömning • för 16 minuter sedan')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders subtitle with created date', () => {
|
|
||||||
const itemWithoutModifiedDate = {
|
|
||||||
...defaultItem,
|
|
||||||
dateModified: undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
const screen = setup({ item: itemWithoutModifiedDate })
|
|
||||||
|
|
||||||
expect(screen.getByText('Bedömning • för 17 minuter sedan')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders subtitle without date', () => {
|
|
||||||
const itemWithoutDate = {
|
|
||||||
...defaultItem,
|
|
||||||
dateCreated: undefined,
|
|
||||||
dateModified: undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
const screen = setup({ item: itemWithoutDate })
|
|
||||||
|
|
||||||
expect(screen.getByText('Bedömning')).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders subtitle without category', () => {
|
|
||||||
const itemWithoutCategory = {
|
|
||||||
...defaultItem,
|
|
||||||
category: undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
const screen = setup({ item: itemWithoutCategory })
|
|
||||||
|
|
||||||
expect(screen.getByText('för 16 minuter sedan')).toBeTruthy()
|
|
||||||
})
|
|
|
@ -1,133 +0,0 @@
|
||||||
import { fireEvent } from '@testing-library/react-native'
|
|
||||||
import React from 'react'
|
|
||||||
import RNCalendarEvents from 'react-native-calendar-events'
|
|
||||||
import Toast from 'react-native-simple-toast'
|
|
||||||
import { render } from '../../utils/testHelpers'
|
|
||||||
import { SaveToCalendar } from '../saveToCalendar.component'
|
|
||||||
|
|
||||||
const defaultEvent = {
|
|
||||||
title: 'Utvecklingssamtal',
|
|
||||||
startDate: '2021-06-19 13:00',
|
|
||||||
endDate: '2021-06-19 14:00',
|
|
||||||
location: 'Gubbängsskolan',
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
event: defaultEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
const setup = (customProps = {}) => {
|
|
||||||
const props = {
|
|
||||||
...defaultProps,
|
|
||||||
...customProps,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(<SaveToCalendar {...props} />)
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
// Hide errors from state illegal state transition
|
|
||||||
// Probably due to mock
|
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {
|
|
||||||
// noop
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(jest.clearAllMocks)
|
|
||||||
|
|
||||||
test('renders save to calendar', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('actionsButton'))
|
|
||||||
|
|
||||||
expect(screen.getByText(/Spara/i)).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('requests calendar permissons', () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('actionsButton'))
|
|
||||||
fireEvent.press(screen.getByText(/Spara/i))
|
|
||||||
|
|
||||||
expect(RNCalendarEvents.requestPermissions).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('can save an event to the calendar', async () => {
|
|
||||||
const screen = setup({
|
|
||||||
event: {
|
|
||||||
...defaultEvent,
|
|
||||||
location: null,
|
|
||||||
description: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('actionsButton'))
|
|
||||||
fireEvent.press(screen.getByText(/Spara/i))
|
|
||||||
await RNCalendarEvents.requestPermissions()
|
|
||||||
|
|
||||||
expect(RNCalendarEvents.saveEvent).toHaveBeenCalledWith('Utvecklingssamtal', {
|
|
||||||
startDate: '2021-06-19T11:00:00.000Z',
|
|
||||||
endDate: '2021-06-19T12:00:00.000Z',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('removes any null values from the event', async () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('actionsButton'))
|
|
||||||
fireEvent.press(screen.getByText(/Spara/i))
|
|
||||||
await RNCalendarEvents.requestPermissions()
|
|
||||||
|
|
||||||
expect(RNCalendarEvents.saveEvent).toHaveBeenCalledWith('Utvecklingssamtal', {
|
|
||||||
startDate: '2021-06-19T11:00:00.000Z',
|
|
||||||
endDate: '2021-06-19T12:00:00.000Z',
|
|
||||||
location: 'Gubbängsskolan',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('calls toast with success', async () => {
|
|
||||||
const screen = setup()
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('actionsButton'))
|
|
||||||
fireEvent.press(screen.getByText(/Spara/i))
|
|
||||||
await RNCalendarEvents.requestPermissions()
|
|
||||||
await RNCalendarEvents.saveEvent()
|
|
||||||
|
|
||||||
expect(Toast.showWithGravity).toHaveBeenCalledWith(
|
|
||||||
'✔️ Sparad till kalender',
|
|
||||||
'short',
|
|
||||||
'bottom'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('says if something goes wrong', async () => {
|
|
||||||
const screen = setup()
|
|
||||||
RNCalendarEvents.saveEvent.mockRejectedValueOnce()
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('actionsButton'))
|
|
||||||
fireEvent.press(screen.getByText(/Spara/i))
|
|
||||||
await RNCalendarEvents.requestPermissions()
|
|
||||||
await RNCalendarEvents.saveEvent()
|
|
||||||
|
|
||||||
expect(Toast.showWithGravity).toHaveBeenCalledWith(
|
|
||||||
'Något gick fel',
|
|
||||||
'short',
|
|
||||||
'bottom'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('tells user if they havent authorized calendar', async () => {
|
|
||||||
const screen = setup()
|
|
||||||
RNCalendarEvents.requestPermissions.mockResolvedValueOnce('not auth')
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByTestId('actionsButton'))
|
|
||||||
fireEvent.press(screen.getByText(/Spara/i))
|
|
||||||
await RNCalendarEvents.requestPermissions()
|
|
||||||
await RNCalendarEvents.saveEvent()
|
|
||||||
|
|
||||||
expect(Toast.showWithGravity).toHaveBeenCalledWith(
|
|
||||||
'Du måste godkänna åtkomst till kalendern',
|
|
||||||
'short',
|
|
||||||
'bottom'
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,280 +0,0 @@
|
||||||
import { RouteProp, useRoute } from '@react-navigation/native'
|
|
||||||
import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
|
|
||||||
import { useUser } from '../libs/hooks/src'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
CheckBox,
|
|
||||||
Input,
|
|
||||||
StyleService,
|
|
||||||
Text,
|
|
||||||
useStyleSheet,
|
|
||||||
} from '@ui-kitten/components'
|
|
||||||
import { Formik } from 'formik'
|
|
||||||
import moment from 'moment'
|
|
||||||
import Personnummer from 'personnummer'
|
|
||||||
import React, { useCallback } from 'react'
|
|
||||||
import { View } from 'react-native'
|
|
||||||
import DateTimePickerModal from 'react-native-modal-datetime-picker'
|
|
||||||
import * as Yup from 'yup'
|
|
||||||
import { defaultStackStyling } from '../design/navigationThemes'
|
|
||||||
// import usePersonalStorage from '../hooks/usePersonalStorage';
|
|
||||||
import useSettingsStorage from '../hooks/useSettingsStorage'
|
|
||||||
import { Layout as LayoutStyle, Sizing, Typography } from '../styles'
|
|
||||||
import { studentName } from '../utils/peopleHelpers'
|
|
||||||
import { useSMS } from '../utils/SMS'
|
|
||||||
import { translate } from '../utils/translation'
|
|
||||||
import { AlertIcon } from './icon.component'
|
|
||||||
import { RootStackParamList } from './navigation.component'
|
|
||||||
import { NavigationTitle } from './navigationTitle.component'
|
|
||||||
|
|
||||||
type AbsenceRouteProps = RouteProp<RootStackParamList, 'Absence'>
|
|
||||||
|
|
||||||
interface AbsenceFormValues {
|
|
||||||
displayStartTimePicker: boolean
|
|
||||||
displayEndTimePicker: boolean
|
|
||||||
personalIdentityNumber: string
|
|
||||||
isFullDay: boolean
|
|
||||||
startTime: moment.Moment
|
|
||||||
endTime: moment.Moment
|
|
||||||
}
|
|
||||||
|
|
||||||
export const absenceRouteOptions =
|
|
||||||
(darkMode: boolean) =>
|
|
||||||
({
|
|
||||||
route,
|
|
||||||
}: {
|
|
||||||
route: RouteProp<RootStackParamList, 'Absence'>
|
|
||||||
}): NativeStackNavigationOptions => {
|
|
||||||
const child = route.params.child
|
|
||||||
return {
|
|
||||||
...defaultStackStyling(darkMode),
|
|
||||||
headerTitle: () => (
|
|
||||||
<NavigationTitle
|
|
||||||
title={translate('abscense.title')}
|
|
||||||
subtitle={studentName(child?.name)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Absence = () => {
|
|
||||||
const AbsenceSchema = Yup.object().shape({
|
|
||||||
personalIdentityNumber: Yup.string()
|
|
||||||
.required(translate('abscense.personalNumberMissing'))
|
|
||||||
.test('is-valid', translate('abscense.invalidPersonalNumber'), (value) =>
|
|
||||||
value ? Personnummer.valid(value) : true
|
|
||||||
),
|
|
||||||
isFullDay: Yup.bool().required(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data: user } = useUser()
|
|
||||||
const route = useRoute<AbsenceRouteProps>()
|
|
||||||
const { sendSMS } = useSMS()
|
|
||||||
const { child } = route.params
|
|
||||||
const [personalIdentityNumber, setPersonalIdentityNumber] = React.useState('')
|
|
||||||
const [personalIdsFromStorage, setPersonalIdInStorage] = useSettingsStorage(
|
|
||||||
'childPersonalIdentityNumber'
|
|
||||||
)
|
|
||||||
const personalIdKey = `@childPersonalIdNumber.${child.id}`
|
|
||||||
const minumumDate = moment().hours(8).minute(0)
|
|
||||||
const maximumDate = moment().hours(17).minute(0)
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
|
|
||||||
const submit = useCallback(
|
|
||||||
async (values: AbsenceFormValues) => {
|
|
||||||
const personalIdNumber = Personnummer.parse(
|
|
||||||
values.personalIdentityNumber
|
|
||||||
).format()
|
|
||||||
|
|
||||||
if (values.isFullDay) {
|
|
||||||
sendSMS(personalIdNumber)
|
|
||||||
} else {
|
|
||||||
sendSMS(
|
|
||||||
`${personalIdNumber} ${moment(values.startTime).format(
|
|
||||||
'HHmm'
|
|
||||||
)}-${moment(values.endTime).format('HHmm')}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toStore = {
|
|
||||||
...personalIdsFromStorage,
|
|
||||||
...{ [personalIdKey]: personalIdNumber },
|
|
||||||
}
|
|
||||||
setPersonalIdInStorage(toStore)
|
|
||||||
},
|
|
||||||
[personalIdKey, personalIdsFromStorage, sendSMS, setPersonalIdInStorage]
|
|
||||||
)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const personalIdFromStorage = personalIdsFromStorage[personalIdKey] || ''
|
|
||||||
setPersonalIdentityNumber(personalIdFromStorage || '')
|
|
||||||
}, [child, personalIdKey, personalIdsFromStorage, user])
|
|
||||||
|
|
||||||
const initialValues: AbsenceFormValues = {
|
|
||||||
displayStartTimePicker: false,
|
|
||||||
displayEndTimePicker: false,
|
|
||||||
personalIdentityNumber: personalIdentityNumber || '',
|
|
||||||
isFullDay: true,
|
|
||||||
startTime: moment().hours(Math.max(8, new Date().getHours())).minute(0),
|
|
||||||
endTime: maximumDate,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
enableReinitialize
|
|
||||||
validationSchema={AbsenceSchema}
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={submit}
|
|
||||||
>
|
|
||||||
{({
|
|
||||||
handleChange,
|
|
||||||
handleBlur,
|
|
||||||
handleSubmit,
|
|
||||||
setFieldValue,
|
|
||||||
values,
|
|
||||||
touched,
|
|
||||||
errors,
|
|
||||||
}) => {
|
|
||||||
const hasError = (field: keyof typeof values) =>
|
|
||||||
errors[field] && touched[field]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.wrap}>
|
|
||||||
<View style={styles.field}>
|
|
||||||
<Text style={styles.label}>
|
|
||||||
{translate('abscense.childsPersonalNumber')}
|
|
||||||
</Text>
|
|
||||||
<Input
|
|
||||||
testID="personalIdentityNumberInput"
|
|
||||||
keyboardType="number-pad"
|
|
||||||
onChangeText={handleChange('personalIdentityNumber')}
|
|
||||||
onBlur={handleBlur('personalIdentityNumber')}
|
|
||||||
status={hasError('personalIdentityNumber') ? 'danger' : 'basic'}
|
|
||||||
value={values.personalIdentityNumber}
|
|
||||||
style={styles.input}
|
|
||||||
placeholder="YYYYMMDD-XXXX"
|
|
||||||
accessoryRight={
|
|
||||||
hasError('personalIdentityNumber') ? AlertIcon : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{hasError('personalIdentityNumber') && (
|
|
||||||
<Text style={styles.error}>
|
|
||||||
{errors.personalIdentityNumber}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View style={styles.field}>
|
|
||||||
<CheckBox
|
|
||||||
checked={values.isFullDay}
|
|
||||||
onChange={(checked) => setFieldValue('isFullDay', checked)}
|
|
||||||
>
|
|
||||||
{translate('abscense.entireDay')}
|
|
||||||
</CheckBox>
|
|
||||||
</View>
|
|
||||||
{!values.isFullDay && (
|
|
||||||
<View style={styles.partOfDay}>
|
|
||||||
<View style={styles.inputHalf}>
|
|
||||||
<Text style={styles.label}>
|
|
||||||
{translate('abscense.startTime')}
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
status="basic"
|
|
||||||
style={styles.pickerButton}
|
|
||||||
onPress={() =>
|
|
||||||
setFieldValue('displayStartTimePicker', true)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{moment(values.startTime).format('LT')}
|
|
||||||
</Button>
|
|
||||||
<DateTimePickerModal
|
|
||||||
cancelTextIOS={translate('general.cancel')}
|
|
||||||
confirmTextIOS={translate('general.confirm')}
|
|
||||||
date={moment(values.startTime).toDate()}
|
|
||||||
isVisible={values.displayStartTimePicker}
|
|
||||||
locale="sv-SE"
|
|
||||||
maximumDate={maximumDate.toDate()}
|
|
||||||
minimumDate={minumumDate.toDate()}
|
|
||||||
minuteInterval={10}
|
|
||||||
mode="time"
|
|
||||||
onConfirm={(date) => {
|
|
||||||
setFieldValue('startTime', date)
|
|
||||||
setFieldValue('displayStartTimePicker', false)
|
|
||||||
}}
|
|
||||||
onCancel={() =>
|
|
||||||
setFieldValue('displayStartTimePicker', false)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={styles.spacer} />
|
|
||||||
<View style={styles.inputHalf}>
|
|
||||||
<Text style={styles.label}>
|
|
||||||
{translate('abscense.endTime')}
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
status="basic"
|
|
||||||
style={styles.pickerButton}
|
|
||||||
onPress={() => setFieldValue('displayEndTimePicker', true)}
|
|
||||||
>
|
|
||||||
{moment(values.endTime).format('LT')}
|
|
||||||
</Button>
|
|
||||||
<DateTimePickerModal
|
|
||||||
cancelTextIOS={translate('general.cancel')}
|
|
||||||
confirmTextIOS={translate('general.confirm')}
|
|
||||||
date={moment(values.endTime).toDate()}
|
|
||||||
isVisible={values.displayEndTimePicker}
|
|
||||||
// Todo fix this
|
|
||||||
locale="sv-SE"
|
|
||||||
maximumDate={maximumDate.toDate()}
|
|
||||||
minimumDate={minumumDate.toDate()}
|
|
||||||
minuteInterval={10}
|
|
||||||
mode="time"
|
|
||||||
onConfirm={(date) => {
|
|
||||||
setFieldValue('endTime', date)
|
|
||||||
setFieldValue('displayEndTimePicker', false)
|
|
||||||
}}
|
|
||||||
onCancel={() =>
|
|
||||||
setFieldValue('displayEndTimePicker', false)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<Button onPress={() => handleSubmit()} status="primary">
|
|
||||||
{translate('general.send')}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Formik>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Absence
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
wrap: {
|
|
||||||
...LayoutStyle.flex.full,
|
|
||||||
padding: Sizing.t4,
|
|
||||||
backgroundColor: 'background-basic-color-2',
|
|
||||||
},
|
|
||||||
field: { marginBottom: Sizing.t4 },
|
|
||||||
partOfDay: { ...LayoutStyle.flex.row, marginBottom: Sizing.t4 },
|
|
||||||
spacer: { width: Sizing.t2 },
|
|
||||||
inputHalf: { ...LayoutStyle.flex.full },
|
|
||||||
input: {
|
|
||||||
backgroundColor: 'background-basic-color-1',
|
|
||||||
borderColor: 'color-input-border',
|
|
||||||
},
|
|
||||||
// TODO: Refactor to use mapping.json in eva design
|
|
||||||
pickerButton: {
|
|
||||||
backgroundColor: 'background-basic-color-1',
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
...Typography.fontSize.sm,
|
|
||||||
...Typography.fontWeight.bold,
|
|
||||||
marginBottom: Sizing.t2,
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
color: 'color-primary-600',
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,166 +0,0 @@
|
||||||
import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
|
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
|
||||||
import {
|
|
||||||
StyleService,
|
|
||||||
Text,
|
|
||||||
useStyleSheet,
|
|
||||||
useTheme,
|
|
||||||
} from '@ui-kitten/components'
|
|
||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
Image,
|
|
||||||
ImageStyle,
|
|
||||||
Keyboard,
|
|
||||||
TouchableOpacity,
|
|
||||||
TouchableWithoutFeedback,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import { useTranslation } from '../hooks/useTranslation'
|
|
||||||
import { Layout as LayoutStyle, Sizing, Typography } from '../styles'
|
|
||||||
import { fontSize } from '../styles/typography'
|
|
||||||
import { KeyboardAvoidingView } from '../ui/keyboardAvoidingView.component'
|
|
||||||
import { SafeAreaView } from '../ui/safeAreaView.component'
|
|
||||||
import { SettingsIcon } from './icon.component'
|
|
||||||
import { Login } from './login.component'
|
|
||||||
import { RootStackParamList } from './navigation.component'
|
|
||||||
|
|
||||||
const randomWord = (
|
|
||||||
t: (scope: I18n.Scope, options?: I18n.TranslateOptions | undefined) => string
|
|
||||||
) => {
|
|
||||||
const words = t('auth.words')
|
|
||||||
const keys = Object.keys(words)
|
|
||||||
|
|
||||||
const randomIndex: number = Math.floor(Math.random() * keys.length)
|
|
||||||
const argumentKey: string = keys[randomIndex]
|
|
||||||
|
|
||||||
return words[argumentKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthProps {
|
|
||||||
navigation: StackNavigationProp<RootStackParamList, 'Login'>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const authRouteOptions = (): NativeStackNavigationOptions => {
|
|
||||||
return {
|
|
||||||
headerShown: false,
|
|
||||||
animationTypeForReplace: 'push',
|
|
||||||
animation: 'fade',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Auth: React.FC<AuthProps> = ({ navigation }) => {
|
|
||||||
const styles = useStyleSheet(themeStyles)
|
|
||||||
const colors = useTheme()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
// const t = (key: string) => key;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SafeAreaView>
|
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
|
||||||
<View style={LayoutStyle.flex.full}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.settingsLink}
|
|
||||||
onPress={() => navigation.navigate('Settings')}
|
|
||||||
accessibilityHint={t(
|
|
||||||
'auth.a11y_navigate_to_settings'
|
|
||||||
// defaultValue: 'Navigerar till vyn för inställningar',
|
|
||||||
)}
|
|
||||||
accessibilityLabel={t(
|
|
||||||
'auth.a11y_settings'
|
|
||||||
// {
|
|
||||||
// // defaultValue: 'Inställningar',
|
|
||||||
// }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<View style={styles.language}>
|
|
||||||
<SettingsIcon
|
|
||||||
height={28}
|
|
||||||
width={28}
|
|
||||||
fill={colors['color-primary-500']}
|
|
||||||
/>
|
|
||||||
<Text style={styles.languageText}>{t('general.settings')}</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<KeyboardAvoidingView>
|
|
||||||
<View style={styles.content}>
|
|
||||||
<View style={styles.imageWrapper}>
|
|
||||||
<Image
|
|
||||||
source={require('../assets/boys.png')}
|
|
||||||
style={styles.image as ImageStyle}
|
|
||||||
accessibilityHint={t(
|
|
||||||
'login.a11y_image_two_boys'
|
|
||||||
// {
|
|
||||||
// defaultValue: 'Bild på två personer som kollar i mobilen',
|
|
||||||
// }
|
|
||||||
)}
|
|
||||||
resizeMode="contain"
|
|
||||||
accessibilityIgnoresInvertColors={false}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text
|
|
||||||
category="h1"
|
|
||||||
style={styles.header}
|
|
||||||
adjustsFontSizeToFit
|
|
||||||
numberOfLines={2}
|
|
||||||
>
|
|
||||||
Öppna skolplattformen
|
|
||||||
</Text>
|
|
||||||
<Login />
|
|
||||||
<Text category="c2" style={styles.subtitle}>
|
|
||||||
{t('auth.subtitle', {
|
|
||||||
word: randomWord(t),
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</KeyboardAvoidingView>
|
|
||||||
</View>
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
</SafeAreaView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeStyles = StyleService.create({
|
|
||||||
container: {
|
|
||||||
...LayoutStyle.mainAxis.flexStart,
|
|
||||||
...LayoutStyle.crossAxis.flexEnd,
|
|
||||||
padding: Sizing.t6,
|
|
||||||
},
|
|
||||||
imageWrapper: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
...Sizing.aspectRatio(1.5, Sizing.Ratio['4:3']),
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
...LayoutStyle.flex.full,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
width: '100%',
|
|
||||||
marginBottom: Sizing.t5,
|
|
||||||
fontFamily: 'Poppins-Black',
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
width: '100%',
|
|
||||||
textAlign: 'center',
|
|
||||||
...Typography.fontSize.xs,
|
|
||||||
marginTop: Sizing.t5,
|
|
||||||
},
|
|
||||||
language: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: Sizing.t3,
|
|
||||||
paddingLeft: Sizing.t5,
|
|
||||||
},
|
|
||||||
languageText: {
|
|
||||||
...fontSize.sm,
|
|
||||||
marginLeft: Sizing.t1,
|
|
||||||
},
|
|
||||||
settingsLink: {
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,114 +0,0 @@
|
||||||
import { useCalendar } from '../libs/hooks/src'
|
|
||||||
import { CalendarItem } from '@skolplattformen/api'
|
|
||||||
import {
|
|
||||||
Divider,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
StyleService,
|
|
||||||
Text,
|
|
||||||
useStyleSheet,
|
|
||||||
} from '@ui-kitten/components'
|
|
||||||
import moment from 'moment'
|
|
||||||
import React from 'react'
|
|
||||||
import { Layout as LayoutStyle, Sizing, Typography } from '../styles'
|
|
||||||
import { ListRenderItemInfo, RefreshControl, View } from 'react-native'
|
|
||||||
|
|
||||||
import { translate } from '../utils/translation'
|
|
||||||
import { useChild } from './childContext.component'
|
|
||||||
import { CalendarOutlineIcon } from './icon.component'
|
|
||||||
import { SaveToCalendar } from './saveToCalendar.component'
|
|
||||||
import { Week } from './week.component'
|
|
||||||
|
|
||||||
// const translate = (key: string) => key;
|
|
||||||
|
|
||||||
export const Calendar = () => {
|
|
||||||
const child = useChild()
|
|
||||||
const { data, status, reload } = useCalendar(child)
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
|
|
||||||
const formatStartDate = (startDate: moment.MomentInput) => {
|
|
||||||
const date = moment(startDate)
|
|
||||||
const output = `${date.format('dddd')} ${date.format(
|
|
||||||
'll'
|
|
||||||
)} • ${date.fromNow()}`
|
|
||||||
|
|
||||||
// Hack to remove year if it is this year
|
|
||||||
const currentYear = moment().year().toString(10)
|
|
||||||
return output.replace(currentYear, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedData = () => {
|
|
||||||
if (!data) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.sort((a, b) =>
|
|
||||||
a.startDate && b.startDate ? a.startDate.localeCompare(b.startDate) : 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Week child={child} />
|
|
||||||
<List
|
|
||||||
data={sortedData()}
|
|
||||||
ItemSeparatorComponent={Divider}
|
|
||||||
ListEmptyComponent={
|
|
||||||
<View style={styles.emptyState}>
|
|
||||||
<Text style={styles.emptyStateHeadline} category="h6">
|
|
||||||
{translate('calender.emptyHeadline')}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.emptyStateDescription}>
|
|
||||||
{translate('calender.emptyText')}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
renderItem={({ item }: ListRenderItemInfo<CalendarItem>) => (
|
|
||||||
<ListItem
|
|
||||||
disabled={true}
|
|
||||||
title={`${item.title}`}
|
|
||||||
description={(props) => (
|
|
||||||
<Text style={[props?.style, styles.description]}>
|
|
||||||
{formatStartDate(item.startDate)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
accessoryLeft={CalendarOutlineIcon}
|
|
||||||
accessoryRight={() => <SaveToCalendar event={item} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={status === 'loading'}
|
|
||||||
onRefresh={reload}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
container: {
|
|
||||||
backgroundColor: 'background-basic-color-1',
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
...Typography.fontSize.xs,
|
|
||||||
color: 'text-hint-color',
|
|
||||||
},
|
|
||||||
emptyState: {
|
|
||||||
...LayoutStyle.center,
|
|
||||||
...LayoutStyle.flex.full,
|
|
||||||
},
|
|
||||||
emptyStateHeadline: {
|
|
||||||
...Typography.align.center,
|
|
||||||
margin: Sizing.t4,
|
|
||||||
},
|
|
||||||
emptyStateDescription: {
|
|
||||||
...Typography.align.center,
|
|
||||||
lineHeight: 21,
|
|
||||||
paddingHorizontal: Sizing.t3,
|
|
||||||
margin: Sizing.t4,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,216 +0,0 @@
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
|
||||||
import {
|
|
||||||
getFocusedRouteNameFromRoute,
|
|
||||||
RouteProp,
|
|
||||||
useNavigation,
|
|
||||||
useRoute,
|
|
||||||
} from '@react-navigation/native'
|
|
||||||
import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
|
|
||||||
// import {StackNavigationProp} from '@react-navigation/stack';
|
|
||||||
import { Icon } from '@ui-kitten/components'
|
|
||||||
import React, { useEffect } from 'react'
|
|
||||||
// import {StyleProp, TextProps} from 'react-native';
|
|
||||||
import { defaultStackStyling } from '../design/navigationThemes'
|
|
||||||
import { useFeature } from '../hooks/useFeature'
|
|
||||||
import { studentName } from '../utils/peopleHelpers'
|
|
||||||
import { translate } from '../utils/translation'
|
|
||||||
import { Calendar } from './calendar.component'
|
|
||||||
import { ChildProvider } from './childContext.component'
|
|
||||||
import { Classmates } from './classmates.component'
|
|
||||||
import { Menu } from './menu.component'
|
|
||||||
import { RootStackParamList } from './navigation.component'
|
|
||||||
import { NavigationTitle } from './navigationTitle.component'
|
|
||||||
import { NewsList } from './newsList.component'
|
|
||||||
import { NotificationsList } from './notificationsList.component'
|
|
||||||
import { TabBarLabel } from './tabBarLabel.component'
|
|
||||||
|
|
||||||
// const translate = (key: string) => key;
|
|
||||||
|
|
||||||
// type ChildNavigationProp = StackNavigationProp<RootStackParamList, 'Child'>;
|
|
||||||
type ChildRouteProps = RouteProp<RootStackParamList, 'Child'>
|
|
||||||
|
|
||||||
export type ChildTabParamList = {
|
|
||||||
News: undefined
|
|
||||||
Notifications: undefined
|
|
||||||
Calendar: undefined
|
|
||||||
Menu: undefined
|
|
||||||
Classmates: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// interface TabTitleProps {
|
|
||||||
// children: string;
|
|
||||||
// style?: StyleProp<TextProps>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const { Navigator, Screen } = createBottomTabNavigator<ChildTabParamList>()
|
|
||||||
|
|
||||||
const NewsScreen = () => <NewsList />
|
|
||||||
const NotificationsScreen = () => <NotificationsList />
|
|
||||||
const CalendarScreen = () => <Calendar />
|
|
||||||
const MenuScreen = () => <Menu />
|
|
||||||
const ClassmatesScreen = () => <Classmates />
|
|
||||||
|
|
||||||
interface ScreenSettings {
|
|
||||||
NEWS_SCREEN: boolean
|
|
||||||
NOTIFICATIONS_SCREEN: boolean
|
|
||||||
CALENDER_SCREEN: boolean
|
|
||||||
MENU_SCREEN: boolean
|
|
||||||
CLASSMATES_SCREEN: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const TabNavigator = ({
|
|
||||||
initialRouteName = 'News',
|
|
||||||
screenSettings,
|
|
||||||
}: {
|
|
||||||
initialRouteName?: keyof ChildTabParamList
|
|
||||||
screenSettings: ScreenSettings
|
|
||||||
}) => (
|
|
||||||
<Navigator
|
|
||||||
initialRouteName={initialRouteName}
|
|
||||||
screenOptions={({ route }) => {
|
|
||||||
return {
|
|
||||||
tabBarLabel: ({ focused }) => (
|
|
||||||
<TabBarLabel
|
|
||||||
label={getRouteTitleFromName(route.name)}
|
|
||||||
focused={focused}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
tabBarIcon: ({ focused, color }) => {
|
|
||||||
let iconName = 'news'
|
|
||||||
|
|
||||||
if (route.name === 'News') {
|
|
||||||
iconName = focused ? 'book-open' : 'book-open-outline'
|
|
||||||
} else if (route.name === 'Notifications') {
|
|
||||||
iconName = focused ? 'alert-circle' : 'alert-circle-outline'
|
|
||||||
} else if (route.name === 'Calendar') {
|
|
||||||
iconName = focused ? 'calendar' : 'calendar-outline'
|
|
||||||
} else if (route.name === 'Menu') {
|
|
||||||
iconName = focused ? 'clipboard' : 'clipboard-outline'
|
|
||||||
} else if (route.name === 'Classmates') {
|
|
||||||
iconName = focused ? 'people' : 'people-outline'
|
|
||||||
}
|
|
||||||
return <Icon name={iconName} fill={color} height={24} width={24} />
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{screenSettings.NEWS_SCREEN && (
|
|
||||||
<Screen
|
|
||||||
name="News"
|
|
||||||
component={NewsScreen}
|
|
||||||
options={{ title: translate('navigation.news'), headerShown: false }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{screenSettings.NOTIFICATIONS_SCREEN && (
|
|
||||||
<Screen
|
|
||||||
name="Notifications"
|
|
||||||
component={NotificationsScreen}
|
|
||||||
options={{
|
|
||||||
title: translate('navigation.notifications'),
|
|
||||||
headerShown: false,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{screenSettings.CALENDER_SCREEN && (
|
|
||||||
<Screen
|
|
||||||
name="Calendar"
|
|
||||||
component={CalendarScreen}
|
|
||||||
options={{
|
|
||||||
title: translate('navigation.calender'),
|
|
||||||
headerShown: false,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{screenSettings.MENU_SCREEN && (
|
|
||||||
<Screen
|
|
||||||
name="Menu"
|
|
||||||
component={MenuScreen}
|
|
||||||
options={{ title: translate('navigation.menu'), headerShown: false }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{screenSettings.CLASSMATES_SCREEN && (
|
|
||||||
<Screen
|
|
||||||
name="Classmates"
|
|
||||||
component={ClassmatesScreen}
|
|
||||||
options={{
|
|
||||||
title: translate('navigation.classmates'),
|
|
||||||
headerShown: false,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Navigator>
|
|
||||||
)
|
|
||||||
|
|
||||||
const getHeaderTitle = (route: any) => {
|
|
||||||
const routeName =
|
|
||||||
getFocusedRouteNameFromRoute(route) ??
|
|
||||||
route.params.initialRouteName ??
|
|
||||||
'News'
|
|
||||||
return getRouteTitleFromName(routeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRouteTitleFromName = (routeName: string) => {
|
|
||||||
switch (routeName) {
|
|
||||||
case 'News':
|
|
||||||
return translate('navigation.news')
|
|
||||||
case 'Notifications':
|
|
||||||
return translate('navigation.notifications')
|
|
||||||
case 'Calendar':
|
|
||||||
return translate('navigation.calender')
|
|
||||||
case 'Menu':
|
|
||||||
return translate('navigation.menu')
|
|
||||||
case 'Classmates':
|
|
||||||
return translate('navigation.classmates')
|
|
||||||
default:
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const childRouteOptions =
|
|
||||||
(darkMode: boolean) =>
|
|
||||||
({
|
|
||||||
route,
|
|
||||||
}: {
|
|
||||||
route: RouteProp<RootStackParamList, 'Child'>
|
|
||||||
}): NativeStackNavigationOptions => {
|
|
||||||
const { child } = route.params
|
|
||||||
|
|
||||||
return {
|
|
||||||
...defaultStackStyling(darkMode),
|
|
||||||
headerTitle: () => (
|
|
||||||
<NavigationTitle
|
|
||||||
title={getHeaderTitle(route)}
|
|
||||||
subtitle={studentName(child?.name)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Child = () => {
|
|
||||||
const route = useRoute<ChildRouteProps>()
|
|
||||||
const { child, initialRouteName } = route.params
|
|
||||||
const useFoodMenu = useFeature('FOOD_MENU')
|
|
||||||
const useClassList = useFeature('CLASS_LIST')
|
|
||||||
|
|
||||||
const navigation = useNavigation()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
navigation.setOptions({ title: getHeaderTitle(route) })
|
|
||||||
}, [navigation, route])
|
|
||||||
|
|
||||||
const screenSettings: ScreenSettings = {
|
|
||||||
NEWS_SCREEN: true,
|
|
||||||
NOTIFICATIONS_SCREEN: true,
|
|
||||||
CALENDER_SCREEN: true,
|
|
||||||
MENU_SCREEN: useFoodMenu,
|
|
||||||
CLASSMATES_SCREEN: useClassList,
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ChildProvider child={child}>
|
|
||||||
<TabNavigator
|
|
||||||
screenSettings={screenSettings}
|
|
||||||
initialRouteName={initialRouteName as keyof ChildTabParamList}
|
|
||||||
/>
|
|
||||||
</ChildProvider>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { Child } from '@skolplattformen/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)
|
|
|
@ -1,337 +0,0 @@
|
||||||
import { useNavigation } from '@react-navigation/native'
|
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
|
||||||
import { Child } from '@skolplattformen/api'
|
|
||||||
import {
|
|
||||||
useCalendar,
|
|
||||||
useClassmates,
|
|
||||||
useMenu,
|
|
||||||
useNews,
|
|
||||||
useNotifications,
|
|
||||||
useSchedule,
|
|
||||||
} from '../libs/hooks/src'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
StyleService,
|
|
||||||
Text,
|
|
||||||
useStyleSheet,
|
|
||||||
} from '@ui-kitten/components'
|
|
||||||
import moment, { Moment } from 'moment'
|
|
||||||
import React, { useEffect } from 'react'
|
|
||||||
import { Pressable, useColorScheme, View } from 'react-native'
|
|
||||||
import { useTranslation } from '../hooks/useTranslation'
|
|
||||||
import { Colors, Layout, Sizing } from '../styles'
|
|
||||||
import { getMeaningfulStartingDate } from '../utils/calendarHelpers'
|
|
||||||
import { studentName } from '../utils/peopleHelpers'
|
|
||||||
import { DaySummary } from './daySummary.component'
|
|
||||||
import { AlertIcon, RightArrowIcon } from './icon.component'
|
|
||||||
import { RootStackParamList } from './navigation.component'
|
|
||||||
import { StudentAvatar } from './studentAvatar.component'
|
|
||||||
|
|
||||||
interface ChildListItemProps {
|
|
||||||
child: Child
|
|
||||||
color: string
|
|
||||||
updated: string
|
|
||||||
currentDate?: Moment
|
|
||||||
}
|
|
||||||
type ChildListItemNavigationProp = StackNavigationProp<
|
|
||||||
RootStackParamList,
|
|
||||||
'Children'
|
|
||||||
>
|
|
||||||
|
|
||||||
export const ChildListItem = ({
|
|
||||||
child,
|
|
||||||
color,
|
|
||||||
updated,
|
|
||||||
currentDate = moment(),
|
|
||||||
}: ChildListItemProps) => {
|
|
||||||
// Forces rerender when child.id changes
|
|
||||||
React.useEffect(() => {
|
|
||||||
// noop
|
|
||||||
}, [child.id])
|
|
||||||
|
|
||||||
const navigation = useNavigation<ChildListItemNavigationProp>()
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const { data: notifications, reload: notificationsReload } =
|
|
||||||
useNotifications(child)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { data: news, status: newsStatus, reload: newsReload } = useNews(child)
|
|
||||||
const { data: classmates, reload: classmatesReload } = useClassmates(child)
|
|
||||||
const { data: calendar, reload: calendarReload } = useCalendar(child)
|
|
||||||
const { data: menu, reload: menuReload } = useMenu(child)
|
|
||||||
const { data: schedule, reload: scheduleReload } = useSchedule(
|
|
||||||
child,
|
|
||||||
moment(currentDate).toISOString(),
|
|
||||||
moment(currentDate).add(7, 'days').toISOString()
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Do not refresh if updated is empty (first render of component)
|
|
||||||
if (updated === '') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newsReload()
|
|
||||||
classmatesReload()
|
|
||||||
notificationsReload()
|
|
||||||
calendarReload()
|
|
||||||
menuReload()
|
|
||||||
scheduleReload()
|
|
||||||
|
|
||||||
// Without eslint-disable below we get into a forever loop
|
|
||||||
// because the function pointers to reload functions change on every reload.
|
|
||||||
// I do not know a workaround for this.
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [updated])
|
|
||||||
|
|
||||||
const notificationsThisWeek = notifications.filter(
|
|
||||||
({ dateCreated, dateModified }) => {
|
|
||||||
const date = dateModified || dateCreated
|
|
||||||
return date ? moment(date).isSame(moment(), 'week') : false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const newsThisWeek = news.filter(({ modified, published }) => {
|
|
||||||
const newsDate = modified || published
|
|
||||||
return newsDate ? moment(newsDate).isSame(currentDate, 'week') : false
|
|
||||||
})
|
|
||||||
|
|
||||||
const scheduleAndCalendarThisWeek = [
|
|
||||||
...(calendar ?? []),
|
|
||||||
...(schedule ?? []),
|
|
||||||
].filter(({ startDate }) =>
|
|
||||||
startDate
|
|
||||||
? moment(startDate).isBetween(
|
|
||||||
moment(currentDate).startOf('day'),
|
|
||||||
moment(currentDate).add(7, 'days')
|
|
||||||
)
|
|
||||||
: false
|
|
||||||
)
|
|
||||||
|
|
||||||
const displayDate = (inputDate: moment.MomentInput) => {
|
|
||||||
return moment(inputDate).fromNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getClassName = () => {
|
|
||||||
// hack: we can find the class name (ex. 8C) from the classmates.
|
|
||||||
// let's pick the first one and select theirs class
|
|
||||||
// hack 2: we can find school namn in skola24 if child data is there
|
|
||||||
if (classmates.length > 0) {
|
|
||||||
return (
|
|
||||||
classmates[0].className +
|
|
||||||
(child.schoolID == null ? '' : ' • ' + child.schoolID)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Taken from Skolverket
|
|
||||||
// https://www.skolverket.se/skolutveckling/anordna-och-administrera-utbildning/administrera-utbildning/skoltermer-pa-engelska
|
|
||||||
const abbrevations = {
|
|
||||||
G: t('abbrevations.upperSecondarySchool'),
|
|
||||||
GR: t('abbrevations.compulsorySchool'),
|
|
||||||
F: t('abbrevations.leisureTimeCentre'),
|
|
||||||
FS: t('abbrevations.preSchool'),
|
|
||||||
}
|
|
||||||
|
|
||||||
return child.status
|
|
||||||
? child.status
|
|
||||||
.split(';')
|
|
||||||
.map((status) => {
|
|
||||||
const statusAsAbbreviation = status as keyof typeof abbrevations
|
|
||||||
|
|
||||||
return abbrevations[statusAsAbbreviation] || status
|
|
||||||
})
|
|
||||||
.join(', ')
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = getClassName()
|
|
||||||
const styles = useStyleSheet(themeStyles)
|
|
||||||
const isDarkMode = useColorScheme() === 'dark'
|
|
||||||
const meaningfulStartingDate = getMeaningfulStartingDate(currentDate)
|
|
||||||
|
|
||||||
// Hide menu if we want to show monday but it is not monday yet.
|
|
||||||
// The menu for next week is not available until monday
|
|
||||||
const shouldShowLunchMenu =
|
|
||||||
menu[meaningfulStartingDate.isoWeekday() - 1] &&
|
|
||||||
!(
|
|
||||||
meaningfulStartingDate.isoWeekday() === 1 &&
|
|
||||||
currentDate.isoWeekday() !== 1
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View style={styles.card}>
|
|
||||||
<View style={styles.cardHeader}>
|
|
||||||
<Pressable
|
|
||||||
style={({ pressed }) => [
|
|
||||||
styles.cardHeaderLeft || {},
|
|
||||||
{ opacity: pressed ? 0.5 : 1 },
|
|
||||||
]}
|
|
||||||
onPress={() => navigation.navigate('Child', { child, color })}
|
|
||||||
>
|
|
||||||
<View style={styles.cardHeaderLeft}>
|
|
||||||
<StudentAvatar name={studentName(child.name)} color={color} />
|
|
||||||
<View style={styles.cardHeaderText}>
|
|
||||||
<Text category="h6">{studentName(child.name)}</Text>
|
|
||||||
{className ? <Text category="s1">{className}</Text> : null}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={styles.cardHeaderRight}>
|
|
||||||
<RightArrowIcon
|
|
||||||
style={styles.icon}
|
|
||||||
fill={
|
|
||||||
isDarkMode ? Colors.neutral.gray200 : Colors.neutral.gray800
|
|
||||||
}
|
|
||||||
name="star"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
<Pressable
|
|
||||||
style={({ pressed }) => ['' || {}, { opacity: pressed ? 0.5 : 1 }]}
|
|
||||||
onPress={() =>
|
|
||||||
navigation.navigate('Child', {
|
|
||||||
child,
|
|
||||||
color,
|
|
||||||
initialRouteName: 'Calendar',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DaySummary child={child} date={meaningfulStartingDate} />
|
|
||||||
|
|
||||||
{scheduleAndCalendarThisWeek.slice(0, 3).map((calendarItem, i) => (
|
|
||||||
<Text category="p1" key={i}>
|
|
||||||
{`${calendarItem.title} (${displayDate(calendarItem.startDate)})`}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
|
||||||
style={({ pressed }) => ['' || {}, { opacity: pressed ? 0.5 : 1 }]}
|
|
||||||
onPress={() =>
|
|
||||||
navigation.navigate('Child', {
|
|
||||||
child,
|
|
||||||
color,
|
|
||||||
initialRouteName: 'News',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text category="c2" style={styles.label}>
|
|
||||||
{t('navigation.news')}
|
|
||||||
</Text>
|
|
||||||
{notificationsThisWeek.slice(0, 3).map((notification, i) => (
|
|
||||||
<Text category="p1" key={i}>
|
|
||||||
{notification.message}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
{newsThisWeek.slice(0, 3).map((newsItem, i) => (
|
|
||||||
<Text category="p1" key={i}>
|
|
||||||
{newsItem.header ?? ''}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
{scheduleAndCalendarThisWeek.length ||
|
|
||||||
notificationsThisWeek.length ||
|
|
||||||
newsThisWeek.length ? null : (
|
|
||||||
<Text category="p1" style={styles.noNewNewsItemsText}>
|
|
||||||
{t('news.noNewNewsItemsThisWeek')}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{shouldShowLunchMenu ? (
|
|
||||||
<Pressable
|
|
||||||
style={({ pressed }) => ['' || {}, { opacity: pressed ? 0.5 : 1 }]}
|
|
||||||
onPress={() =>
|
|
||||||
navigation.navigate('Child', {
|
|
||||||
child,
|
|
||||||
color,
|
|
||||||
initialRouteName: 'Menu',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text category="c2" style={styles.label}>
|
|
||||||
{meaningfulStartingDate.format(
|
|
||||||
'[' + t('schedule.lunch') + '] dddd'
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
{menu[meaningfulStartingDate.isoWeekday() - 1]?.description}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<View style={styles.itemFooter}>
|
|
||||||
<Button
|
|
||||||
accessible
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={`${child.name}, ${t('abscense.title')}`}
|
|
||||||
appearance="ghost"
|
|
||||||
accessoryLeft={AlertIcon}
|
|
||||||
status="primary"
|
|
||||||
style={styles.absenceButton}
|
|
||||||
onPress={() => navigation.navigate('Absence', { child })}
|
|
||||||
>
|
|
||||||
{t('abscense.title')}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeStyles = StyleService.create({
|
|
||||||
card: {
|
|
||||||
borderRadius: 25,
|
|
||||||
padding: Sizing.t5,
|
|
||||||
marginBottom: Sizing.t4,
|
|
||||||
backgroundColor: 'background-basic-color-1',
|
|
||||||
},
|
|
||||||
cardHeader: {
|
|
||||||
...Layout.flex.row,
|
|
||||||
...Layout.mainAxis.center,
|
|
||||||
...Layout.crossAxis.spaceBetween,
|
|
||||||
marginBottom: Sizing.t4,
|
|
||||||
},
|
|
||||||
cardHeaderLeft: {
|
|
||||||
...Layout.flex.row,
|
|
||||||
...Layout.mainAxis.center,
|
|
||||||
flex: 10,
|
|
||||||
},
|
|
||||||
cardHeaderRight: {
|
|
||||||
...Layout.flex.row,
|
|
||||||
...Layout.crossAxis.flexEnd,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
cardHeaderText: {
|
|
||||||
marginHorizontal: Sizing.t4,
|
|
||||||
flex: 10,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
marginTop: 10,
|
|
||||||
},
|
|
||||||
itemFooter: {
|
|
||||||
...Layout.flex.row,
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'flex-end',
|
|
||||||
marginTop: Sizing.t4,
|
|
||||||
},
|
|
||||||
absenceButton: {
|
|
||||||
marginLeft: -20,
|
|
||||||
},
|
|
||||||
itemFooterSpinner: {
|
|
||||||
alignSelf: 'flex-end',
|
|
||||||
},
|
|
||||||
item: {
|
|
||||||
marginRight: 12,
|
|
||||||
paddingHorizontal: 2,
|
|
||||||
paddingVertical: 0,
|
|
||||||
marginBottom: 0,
|
|
||||||
},
|
|
||||||
noNewNewsItemsText: {},
|
|
||||||
})
|
|
|
@ -1,214 +0,0 @@
|
||||||
import { NavigationProp, useNavigation } from '@react-navigation/core'
|
|
||||||
import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
|
|
||||||
import { Child } from '@skolplattformen/api'
|
|
||||||
import { useApi, useChildList } from '../libs/hooks/src'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
List,
|
|
||||||
Spinner,
|
|
||||||
StyleService,
|
|
||||||
Text,
|
|
||||||
TopNavigationAction,
|
|
||||||
useStyleSheet,
|
|
||||||
} from '@ui-kitten/components'
|
|
||||||
import moment from 'moment'
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
|
||||||
import {
|
|
||||||
Image,
|
|
||||||
ImageStyle,
|
|
||||||
Linking,
|
|
||||||
ListRenderItemInfo,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import { defaultStackStyling } from '../design/navigationThemes'
|
|
||||||
import AppStorage from '../services/appStorage'
|
|
||||||
import { Layout as LayoutStyle, Sizing, Typography } from '../styles'
|
|
||||||
import { translate } from '../utils/translation'
|
|
||||||
import { ChildListItem } from './childListItem.component'
|
|
||||||
import { RefreshIcon, SettingsIcon } from './icon.component'
|
|
||||||
import { RootStackParamList } from './navigation.component'
|
|
||||||
|
|
||||||
const colors = ['primary', 'success', 'info', 'warning', 'danger']
|
|
||||||
|
|
||||||
export const childenRouteOptions =
|
|
||||||
(darkMode: boolean) => (): NativeStackNavigationOptions => {
|
|
||||||
return {
|
|
||||||
...defaultStackStyling(darkMode),
|
|
||||||
title: translate('children.title'),
|
|
||||||
headerLargeTitle: false,
|
|
||||||
headerLargeTitleShadowVisible: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Children = () => {
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
|
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>()
|
|
||||||
|
|
||||||
const { api } = useApi()
|
|
||||||
const { data: childList, status, reload } = useChildList()
|
|
||||||
const reloadChildren = useCallback(() => {
|
|
||||||
reload()
|
|
||||||
setUpdated(moment().toISOString())
|
|
||||||
}, [reload])
|
|
||||||
|
|
||||||
const [updatedAt, setUpdated] = useState('')
|
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
|
||||||
AppStorage.clearTemporaryItems().then(() => api.logout())
|
|
||||||
}, [api])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
navigation.setOptions({
|
|
||||||
headerLeft: () => {
|
|
||||||
return (
|
|
||||||
<TopNavigationAction
|
|
||||||
icon={SettingsIcon}
|
|
||||||
onPress={() => navigation.navigate('Settings')}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headerRight: () => {
|
|
||||||
return (
|
|
||||||
<TopNavigationAction
|
|
||||||
icon={RefreshIcon}
|
|
||||||
onPress={() => reloadChildren()}
|
|
||||||
accessibilityHint="Reload"
|
|
||||||
accessibilityLabel="Reload"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [navigation, reloadChildren])
|
|
||||||
|
|
||||||
// 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]}
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
topContainer: {
|
|
||||||
...LayoutStyle.flex.full,
|
|
||||||
paddingBottom: 0,
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
...LayoutStyle.center,
|
|
||||||
...LayoutStyle.flex.full,
|
|
||||||
},
|
|
||||||
loadingImage: {
|
|
||||||
...Sizing.aspectRatio(),
|
|
||||||
},
|
|
||||||
loadingMessage: {
|
|
||||||
...LayoutStyle.mainAxis.center,
|
|
||||||
...LayoutStyle.flex.row,
|
|
||||||
marginTop: Sizing.t2,
|
|
||||||
},
|
|
||||||
loadingText: {
|
|
||||||
marginLeft: Sizing.t5,
|
|
||||||
},
|
|
||||||
errorButtons: {
|
|
||||||
height: Sizing.screen.height * 0.2,
|
|
||||||
width: Sizing.screen.width * 0.73,
|
|
||||||
justifyContent: 'space-evenly',
|
|
||||||
},
|
|
||||||
errorMessage: {
|
|
||||||
height: Sizing.screen.height * 0.4,
|
|
||||||
width: Sizing.screen.width * 0.73,
|
|
||||||
justifyContent: 'space-evenly',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: Sizing.t2,
|
|
||||||
},
|
|
||||||
errorText: {
|
|
||||||
marginBottom: Sizing.t3,
|
|
||||||
},
|
|
||||||
childList: {
|
|
||||||
...LayoutStyle.flex.full,
|
|
||||||
},
|
|
||||||
childListContainer: {
|
|
||||||
paddingVertical: Sizing.t4,
|
|
||||||
paddingHorizontal: Sizing.t3,
|
|
||||||
},
|
|
||||||
emptyState: {
|
|
||||||
...LayoutStyle.center,
|
|
||||||
...LayoutStyle.flex.full,
|
|
||||||
paddingHorizontal: Sizing.t5,
|
|
||||||
},
|
|
||||||
emptyStateDescription: {
|
|
||||||
...Typography.align.center,
|
|
||||||
lineHeight: 21,
|
|
||||||
marginTop: Sizing.t2,
|
|
||||||
},
|
|
||||||
emptyStateImage: {
|
|
||||||
...Sizing.aspectRatio(0.8),
|
|
||||||
marginTop: Sizing.t5,
|
|
||||||
},
|
|
||||||
topNavigationTitle: {
|
|
||||||
...Typography.fontWeight.semibold,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,90 +0,0 @@
|
||||||
import { Classmate } from '@skolplattformen/api'
|
|
||||||
import { useClassmates } from '../libs/hooks/src'
|
|
||||||
import {
|
|
||||||
Divider,
|
|
||||||
Icon,
|
|
||||||
IconProps,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
Text,
|
|
||||||
} from '@ui-kitten/components'
|
|
||||||
import React from 'react'
|
|
||||||
import { ListRenderItemInfo, RefreshControl, StyleSheet } from 'react-native'
|
|
||||||
import { fullName, guardians, sortByFirstName } from '../utils/peopleHelpers'
|
|
||||||
import { translate } from '../utils/translation'
|
|
||||||
import { useChild } from './childContext.component'
|
|
||||||
import { ContactMenu } from './contactMenu.component'
|
|
||||||
|
|
||||||
// const translate = (key: string) => key;
|
|
||||||
|
|
||||||
// interface ClassmatesProps {
|
|
||||||
// setSelected: (value?: number | null) => void;
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const Classmates = () => {
|
|
||||||
const child = useChild()
|
|
||||||
|
|
||||||
const { data, status, reload } = useClassmates(child)
|
|
||||||
const renderItemIcon = (props: IconProps) => (
|
|
||||||
<Icon {...props} name="people-outline" />
|
|
||||||
)
|
|
||||||
const [selected, setSelected] = React.useState<Classmate>()
|
|
||||||
const renderItem = ({ item, index }: ListRenderItemInfo<Classmate>) => (
|
|
||||||
<ListItem
|
|
||||||
accessibilityLabel={`${translate('classmates.child')} ${index + 1}`}
|
|
||||||
accessibilityHint={`${translate(
|
|
||||||
'classmates.contactsForGuardianFor'
|
|
||||||
)} ${fullName(item)}`}
|
|
||||||
title={fullName(item)}
|
|
||||||
onPress={() => setSelected(item)}
|
|
||||||
description={guardians(item.guardians)}
|
|
||||||
accessoryLeft={renderItemIcon}
|
|
||||||
accessoryRight={() => (
|
|
||||||
<ContactMenu
|
|
||||||
contact={item}
|
|
||||||
selected={item === selected}
|
|
||||||
setSelected={() => setSelected(undefined)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<List
|
|
||||||
style={styles.container}
|
|
||||||
data={sortByFirstName(data)}
|
|
||||||
ItemSeparatorComponent={Divider}
|
|
||||||
ListHeaderComponent={
|
|
||||||
<Text category="h5" style={styles.listHeader}>
|
|
||||||
{data?.length
|
|
||||||
? `${translate('classmates.class')} ${data[0].className}`
|
|
||||||
: translate('classmates.class')}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
renderItem={renderItem}
|
|
||||||
contentContainerStyle={styles.contentContainer}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl refreshing={status === 'loading'} onRefresh={reload} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
contentContainer: {
|
|
||||||
margin: 10,
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
},
|
|
||||||
topContainer: {
|
|
||||||
margin: 5,
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
listHeader: {
|
|
||||||
paddingTop: 10,
|
|
||||||
paddingLeft: 15,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,130 +0,0 @@
|
||||||
import { Classmate } from '@skolplattformen/api'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
MenuGroup,
|
|
||||||
MenuItem,
|
|
||||||
OverflowMenu,
|
|
||||||
} from '@ui-kitten/components'
|
|
||||||
import React from 'react'
|
|
||||||
import { Linking, StyleSheet } from 'react-native'
|
|
||||||
import { fullName } from '../utils/peopleHelpers'
|
|
||||||
import { translate } from '../utils/translation'
|
|
||||||
import {
|
|
||||||
CallIcon,
|
|
||||||
EmailIcon,
|
|
||||||
MapIcon,
|
|
||||||
MoreIcon,
|
|
||||||
SMSIcon,
|
|
||||||
} from './icon.component'
|
|
||||||
|
|
||||||
interface ContactMenuProps {
|
|
||||||
contact: Classmate
|
|
||||||
selected: boolean
|
|
||||||
setSelected: (value?: number | null) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// const translate = (key: string) => key;
|
|
||||||
|
|
||||||
export const ContactMenu = ({
|
|
||||||
contact,
|
|
||||||
selected,
|
|
||||||
setSelected,
|
|
||||||
}: ContactMenuProps) => {
|
|
||||||
const [visible, setVisible] = React.useState(selected)
|
|
||||||
|
|
||||||
const renderToggleButton = () => (
|
|
||||||
<Button
|
|
||||||
testID="ShowContactInfoButton"
|
|
||||||
accessibilityHint={translate(
|
|
||||||
'contact.a11y_show_contact_info_button_hint'
|
|
||||||
// {
|
|
||||||
// defaultValue: 'Visar kontaktinformation',
|
|
||||||
// },
|
|
||||||
)}
|
|
||||||
accessibilityLabel={translate(
|
|
||||||
'contact.a11y_show_contact_info_button_label'
|
|
||||||
// {
|
|
||||||
// defaultValue: 'Visa kontaktinformation',
|
|
||||||
// },
|
|
||||||
)}
|
|
||||||
onPress={() => {
|
|
||||||
setVisible(true)
|
|
||||||
}}
|
|
||||||
appearance="ghost"
|
|
||||||
accessoryLeft={MoreIcon}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleBackdropPress = () => {
|
|
||||||
setVisible(false)
|
|
||||||
setSelected(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldDisplay = (option?: string) => (option ? 'flex' : 'none')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OverflowMenu
|
|
||||||
visible={visible}
|
|
||||||
anchor={renderToggleButton}
|
|
||||||
backdropStyle={styles.backdrop}
|
|
||||||
onBackdropPress={handleBackdropPress}
|
|
||||||
>
|
|
||||||
{contact.guardians.map((guardian) => {
|
|
||||||
const { address, email, mobile } = guardian
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuGroup
|
|
||||||
key={fullName(guardian)}
|
|
||||||
title={fullName(guardian)}
|
|
||||||
style={styles.group}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
testID="CallMenuItem"
|
|
||||||
accessibilityLabel={translate('contact.call')}
|
|
||||||
accessoryLeft={CallIcon}
|
|
||||||
style={{ display: shouldDisplay(mobile) }}
|
|
||||||
title={translate('contact.call')}
|
|
||||||
onPress={() => Linking.openURL(`tel:${mobile}`)}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
testID="SMSMenuItem"
|
|
||||||
accessibilityLabel={translate('contact.sms')}
|
|
||||||
accessoryLeft={SMSIcon}
|
|
||||||
style={{ display: shouldDisplay(mobile) }}
|
|
||||||
title={translate('contact.sms')}
|
|
||||||
onPress={() => Linking.openURL(`sms:${mobile}`)}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
testID="SendEmailMenuItem"
|
|
||||||
accessibilityLabel={translate('contact.email')}
|
|
||||||
accessoryLeft={EmailIcon}
|
|
||||||
style={{ display: shouldDisplay(email) }}
|
|
||||||
title={translate('contact.email')}
|
|
||||||
onPress={() => Linking.openURL(`mailto:${email}`)}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
testID="ShowHomeMenuItem"
|
|
||||||
accessibilityLabel={translate('contact.home')}
|
|
||||||
accessoryLeft={MapIcon}
|
|
||||||
style={{ display: shouldDisplay(address) }}
|
|
||||||
title={translate('contact.home')}
|
|
||||||
onPress={() =>
|
|
||||||
Linking.openURL(`http://maps.apple.com/?daddr=${address}`)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</MenuGroup>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</OverflowMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
backdrop: {
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
},
|
|
||||||
group: {
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 10,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,107 +0,0 @@
|
||||||
import { Child } from '@skolplattformen/api'
|
|
||||||
import { useTimetable } from '../libs/hooks/src'
|
|
||||||
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
|
|
||||||
import moment, { Moment } from 'moment'
|
|
||||||
import React from 'react'
|
|
||||||
import { View } from 'react-native'
|
|
||||||
import { LanguageService } from '../services/languageService'
|
|
||||||
import { translate } from '../utils/translation'
|
|
||||||
|
|
||||||
interface DaySummaryProps {
|
|
||||||
child: Child
|
|
||||||
date?: Moment
|
|
||||||
}
|
|
||||||
|
|
||||||
const capitalizeFirstLetter = (string: string) => {
|
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DaySummary = ({
|
|
||||||
child,
|
|
||||||
date: currentDate = moment(),
|
|
||||||
}: DaySummaryProps) => {
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
const [week, year] = [currentDate.isoWeek(), currentDate.isoWeekYear()]
|
|
||||||
|
|
||||||
const { data: weekLessons } = useTimetable(
|
|
||||||
child,
|
|
||||||
week,
|
|
||||||
year,
|
|
||||||
LanguageService.getLanguageCode()
|
|
||||||
)
|
|
||||||
|
|
||||||
const lessons = weekLessons
|
|
||||||
.filter((lesson) => lesson.dayOfWeek === currentDate.isoWeekday())
|
|
||||||
.sort((a, b) => a.timeStart.localeCompare(b.timeStart))
|
|
||||||
|
|
||||||
if (lessons.length <= 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const gymBag = lessons.some((lesson) => lesson.code === 'IDH')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
{moment().weekday() !== currentDate.weekday() ? (
|
|
||||||
<Text category="c2" style={styles.weekday}>
|
|
||||||
{capitalizeFirstLetter(currentDate.format('dddd'))}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
<View style={styles.summary}>
|
|
||||||
<View style={styles.part}>
|
|
||||||
<View>
|
|
||||||
<Text category="c2" style={styles.label}>
|
|
||||||
{translate('schedule.start')}
|
|
||||||
</Text>
|
|
||||||
<Text category="h5">{lessons[0].timeStart.slice(0, 5)} - </Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={styles.part}>
|
|
||||||
<View>
|
|
||||||
<Text category="c2" style={styles.label}>
|
|
||||||
{translate('schedule.end')}
|
|
||||||
</Text>
|
|
||||||
<Text category="h5">
|
|
||||||
{lessons
|
|
||||||
.sort((a, b) => a.timeEnd.localeCompare(b.timeEnd))
|
|
||||||
[lessons.length - 1].timeEnd.slice(0, 5)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={styles.part}>
|
|
||||||
<View>
|
|
||||||
<Text category="c2" style={styles.label}>
|
|
||||||
|
|
||||||
</Text>
|
|
||||||
<Text category="s2">
|
|
||||||
{gymBag
|
|
||||||
? ` 🤼♀️ ${translate('schedule.gymBag', {
|
|
||||||
defaultValue: 'Gympapåse',
|
|
||||||
})}`
|
|
||||||
: ''}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
part: {
|
|
||||||
flexDirection: 'column',
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
marginTop: 10,
|
|
||||||
},
|
|
||||||
heading: {
|
|
||||||
marginBottom: -10,
|
|
||||||
},
|
|
||||||
weekday: {
|
|
||||||
marginBottom: -10,
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { Icon, IconProps } from '@ui-kitten/components'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const uiIcon = (name: string) => (props: IconProps) =>
|
|
||||||
<Icon {...props} name={name} />
|
|
||||||
|
|
||||||
export const AlertIcon = uiIcon('alert-circle-outline')
|
|
||||||
export const BackIcon = uiIcon('arrow-back')
|
|
||||||
export const BrushIcon = uiIcon('brush')
|
|
||||||
export const CalendarOutlineIcon = uiIcon('calendar-outline')
|
|
||||||
export const CallIcon = uiIcon('phone-outline')
|
|
||||||
export const CheckIcon = uiIcon('checkmark-outline')
|
|
||||||
export const ClassIcon = uiIcon('people-outline')
|
|
||||||
export const CloseIcon = uiIcon('close-circle')
|
|
||||||
export const CloseOutlineIcon = uiIcon('close-outline')
|
|
||||||
export const EmailIcon = uiIcon('email-outline')
|
|
||||||
export const MoreIcon = uiIcon('more-horizontal-outline')
|
|
||||||
export const MapIcon = uiIcon('map-outline')
|
|
||||||
export const MenuIcon = uiIcon('smiling-face-outline')
|
|
||||||
export const NewsIcon = uiIcon('activity-outline')
|
|
||||||
export const NotificationsIcon = uiIcon('alert-circle-outline')
|
|
||||||
export const PersonIcon = uiIcon('person-outline')
|
|
||||||
export const SecureIcon = uiIcon('keypad-outline')
|
|
||||||
export const SelectIcon = uiIcon('arrow-ios-downward-outline')
|
|
||||||
export const SMSIcon = uiIcon('message-square-outline')
|
|
||||||
export const SettingsIcon = uiIcon('settings-outline')
|
|
||||||
export const SearchIcon = uiIcon('search-outline')
|
|
||||||
export const BookOpenIcon = uiIcon('book-open-outline')
|
|
||||||
export const GlobeIcon = uiIcon('globe-outline')
|
|
||||||
export const ExternalLinkIcon = uiIcon('external-link-outline')
|
|
||||||
export const ClipboardIcon = uiIcon('clipboard-outline')
|
|
||||||
export const RightArrowIcon = uiIcon('arrow-ios-forward-outline')
|
|
||||||
export const QuestionMarkIcon = uiIcon('question-mark')
|
|
||||||
export const AwardIcon = uiIcon('award')
|
|
||||||
export const RefreshIcon = uiIcon('refresh')
|
|
|
@ -1,113 +0,0 @@
|
||||||
import { useApi } from '../libs/hooks/src'
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
|
||||||
import {
|
|
||||||
Image as ImageBase,
|
|
||||||
ImageResizeMode,
|
|
||||||
ImageStyle,
|
|
||||||
StyleProp,
|
|
||||||
useWindowDimensions,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
|
|
||||||
interface ImageProps {
|
|
||||||
src: string
|
|
||||||
style: StyleProp<ImageStyle>
|
|
||||||
/**
|
|
||||||
* Width of component. Defaults to window width
|
|
||||||
* Used to automatically calculate width
|
|
||||||
*/
|
|
||||||
componentWidth?: number
|
|
||||||
accessibilityIgnoresInvertColors: boolean
|
|
||||||
resizeMode?: ImageResizeMode
|
|
||||||
width?: number
|
|
||||||
height?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Image = ({
|
|
||||||
src,
|
|
||||||
style,
|
|
||||||
componentWidth = 0,
|
|
||||||
accessibilityIgnoresInvertColors,
|
|
||||||
resizeMode = 'contain',
|
|
||||||
}: ImageProps) => {
|
|
||||||
const { api } = useApi()
|
|
||||||
const [headers, setHeaders] = useState<{ [index: string]: string }>()
|
|
||||||
const { width: windowWidth } = useWindowDimensions()
|
|
||||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
|
||||||
|
|
||||||
const debugImageName = getDebugImageName(src)
|
|
||||||
|
|
||||||
const prefetchImageInformation = useCallback(
|
|
||||||
async (url: string) => {
|
|
||||||
if (!url) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const newHeaders = await api.getSessionHeaders(url)
|
|
||||||
|
|
||||||
/*
|
|
||||||
console.log('[IMAGE] Getting image dimensions with headers', {
|
|
||||||
debugImageName,
|
|
||||||
newHeaders,
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
ImageBase.getSizeWithHeaders(
|
|
||||||
url,
|
|
||||||
newHeaders,
|
|
||||||
(w, h) => {
|
|
||||||
/*
|
|
||||||
console.log('[IMAGE] Received image dimensions', {
|
|
||||||
debugImageName,
|
|
||||||
w,
|
|
||||||
h,
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
setDimensions({ width: w, height: h })
|
|
||||||
setHeaders(newHeaders)
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
console.error('[Image] Failed to get image dimensions', {
|
|
||||||
debugImageName,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[api, debugImageName]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
prefetchImageInformation(src)
|
|
||||||
}, [prefetchImageInformation, src])
|
|
||||||
|
|
||||||
const compWidth = componentWidth || windowWidth
|
|
||||||
|
|
||||||
const scale = compWidth / dimensions.width
|
|
||||||
const scaledWidth = Math.round(dimensions.width * scale)
|
|
||||||
const scaledHeight = Math.round(dimensions.height * scale)
|
|
||||||
|
|
||||||
const imageSource =
|
|
||||||
resizeMode === 'cover'
|
|
||||||
? { uri: src, headers }
|
|
||||||
: { uri: src, headers, height: scaledHeight, width: scaledWidth }
|
|
||||||
|
|
||||||
return headers && scaledWidth && scaledHeight ? (
|
|
||||||
<ImageBase
|
|
||||||
accessibilityIgnoresInvertColors={accessibilityIgnoresInvertColors}
|
|
||||||
source={imageSource}
|
|
||||||
resizeMode={resizeMode}
|
|
||||||
style={style}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<View style={style} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDebugImageName = (src: string) => {
|
|
||||||
try {
|
|
||||||
const split = src.split('/')
|
|
||||||
return split[split.length - 1]
|
|
||||||
} catch (e: any) {
|
|
||||||
console.log('FAILED', e.message)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
import { RouteProp, useRoute } from '@react-navigation/native'
|
|
||||||
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
|
|
||||||
import React from 'react'
|
|
||||||
import { Linking, Platform } from 'react-native'
|
|
||||||
import { ScrollView } from 'react-native-gesture-handler'
|
|
||||||
import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
|
|
||||||
import { Layout, Sizing, Typography } from '../styles'
|
|
||||||
import { fontSize } from '../styles/typography'
|
|
||||||
import { RootStackParamList } from './navigation.component'
|
|
||||||
|
|
||||||
type LibraryRouteProp = RouteProp<RootStackParamList, 'Library'>
|
|
||||||
|
|
||||||
export const libraryRouteOptions = (): NativeStackNavigationOptions => {
|
|
||||||
return {
|
|
||||||
title: '',
|
|
||||||
headerLargeTitle: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LibraryScreen = () => {
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
const route = useRoute<LibraryRouteProp>()
|
|
||||||
const library = route.params.library
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView
|
|
||||||
contentContainerStyle={styles.article}
|
|
||||||
style={styles.scrollView}
|
|
||||||
>
|
|
||||||
<Text style={styles.title}>
|
|
||||||
{library.libraryName}
|
|
||||||
<Text style={styles.version}> (v{library.version})</Text>
|
|
||||||
</Text>
|
|
||||||
{library._description && (
|
|
||||||
<Text style={styles.description}>{library._description}</Text>
|
|
||||||
)}
|
|
||||||
<Text style={styles.license}>
|
|
||||||
{library._licenseContent ?? library._license?.toString()}
|
|
||||||
</Text>
|
|
||||||
{library.homepage && (
|
|
||||||
<Text
|
|
||||||
style={styles.link}
|
|
||||||
onPress={() => Linking.openURL(library.homepage ?? '')}
|
|
||||||
>
|
|
||||||
{library.homepage}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
title: {
|
|
||||||
...Typography.fontWeight.bold,
|
|
||||||
fontSize: 30,
|
|
||||||
marginBottom: Sizing.t4,
|
|
||||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
|
||||||
...fontSize.lg,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: 'text-hint-color',
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
fontWeight: '700',
|
|
||||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
|
||||||
textDecorationColor: 'text-hint-color',
|
|
||||||
textDecorationLine: 'underline',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
|
||||||
fontWeight: '700',
|
|
||||||
marginBottom: Sizing.t4,
|
|
||||||
},
|
|
||||||
license: {
|
|
||||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
|
||||||
marginBottom: Sizing.t4,
|
|
||||||
},
|
|
||||||
article: {
|
|
||||||
padding: Sizing.t5,
|
|
||||||
},
|
|
||||||
scrollView: {
|
|
||||||
...Layout.flex.full,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { StyleService, useStyleSheet } from '@ui-kitten/components'
|
|
||||||
import { Library } from 'libraries.json'
|
|
||||||
import React, { useCallback } from 'react'
|
|
||||||
import { FlatList, ListRenderItemInfo } from 'react-native'
|
|
||||||
import { Layout as LayoutStyle, Sizing } from '../styles'
|
|
||||||
import { LibraryListItem } from './libraryListItem.component'
|
|
||||||
import { SettingListSeparator } from './settingsComponents.component'
|
|
||||||
|
|
||||||
export const LibraryList = ({ libraries }: { libraries: Library[] }) => {
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
const renderItem = useCallback(
|
|
||||||
({ item: library }: ListRenderItemInfo<Library>) => (
|
|
||||||
<LibraryListItem library={library} />
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const keyExtractor = useCallback((library: Library) => {
|
|
||||||
return `${library.libraryName}:${library.version}`
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
data={libraries}
|
|
||||||
renderItem={renderItem}
|
|
||||||
keyExtractor={keyExtractor}
|
|
||||||
ItemSeparatorComponent={SettingListSeparator}
|
|
||||||
style={styles.list}
|
|
||||||
contentContainerStyle={styles.container}
|
|
||||||
initialNumToRender={15}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
list: {
|
|
||||||
...LayoutStyle.flex.full,
|
|
||||||
paddingHorizontal: Sizing.t4,
|
|
||||||
marginBottom: Sizing.t5,
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
borderRadius: 15,
|
|
||||||
backgroundColor: 'background-basic-color-1',
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { NavigationProp, useNavigation } from '@react-navigation/core'
|
|
||||||
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
|
|
||||||
import { Library } from 'libraries.json'
|
|
||||||
import React from 'react'
|
|
||||||
import { Platform, View } from 'react-native'
|
|
||||||
import { fontSize } from '../styles/typography'
|
|
||||||
import { RootStackParamList } from './navigation.component'
|
|
||||||
import { SettingListItem } from './settingsComponents.component'
|
|
||||||
|
|
||||||
export const LibraryListItem = ({ library }: { library: Library }) => {
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>()
|
|
||||||
return (
|
|
||||||
<SettingListItem
|
|
||||||
onNavigate={() => navigation.navigate('Library', { library })}
|
|
||||||
>
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text style={styles.name}>{library.libraryName}</Text>
|
|
||||||
<View style={styles.bottomRow}>
|
|
||||||
<Text style={styles.version}>v{library.version}</Text>
|
|
||||||
<Text style={styles.license}>
|
|
||||||
{library._license?.toString() ?? 'Unknown'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</SettingListItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
container: {},
|
|
||||||
name: {
|
|
||||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
license: {
|
|
||||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
|
||||||
fontWeight: '700',
|
|
||||||
marginLeft: 10,
|
|
||||||
color: 'text-hint-color',
|
|
||||||
...fontSize.sm,
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
|
||||||
minWidth: 55,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: 'text-hint-color',
|
|
||||||
...fontSize.sm,
|
|
||||||
},
|
|
||||||
bottomRow: {
|
|
||||||
marginTop: 4,
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { StyleService, useStyleSheet } from '@ui-kitten/components'
|
|
||||||
import React from 'react'
|
|
||||||
import { ActivityIndicator, View } from 'react-native'
|
|
||||||
|
|
||||||
export const LoadingComponent = () => {
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[styles.container, styles.horizontal]}>
|
|
||||||
<ActivityIndicator size="large" />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: 'background-basic-color-2',
|
|
||||||
},
|
|
||||||
horizontal: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-around',
|
|
||||||
padding: 10,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,421 +0,0 @@
|
||||||
import { useApi } from '../libs/hooks/src'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
Card,
|
|
||||||
Divider,
|
|
||||||
Input,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
Modal,
|
|
||||||
StyleService,
|
|
||||||
Text,
|
|
||||||
useStyleSheet,
|
|
||||||
} from '@ui-kitten/components'
|
|
||||||
import Personnummer from 'personnummer'
|
|
||||||
import React, { useContext, useEffect, useState } from 'react'
|
|
||||||
import {
|
|
||||||
Image,
|
|
||||||
Linking,
|
|
||||||
Platform,
|
|
||||||
TouchableWithoutFeedback,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import { schema } from '../app.json'
|
|
||||||
import { SchoolPlatformContext } from '../context/schoolPlatform/schoolPlatformContext'
|
|
||||||
import { schoolPlatforms } from '../data/schoolPlatforms'
|
|
||||||
import { useFeature } from '../hooks/useFeature'
|
|
||||||
import useSettingsStorage from '../hooks/useSettingsStorage'
|
|
||||||
import { useTranslation } from '../hooks/useTranslation'
|
|
||||||
import { Layout } from '../styles'
|
|
||||||
import {
|
|
||||||
CheckIcon,
|
|
||||||
CloseOutlineIcon,
|
|
||||||
PersonIcon,
|
|
||||||
SelectIcon,
|
|
||||||
} from './icon.component'
|
|
||||||
import AppStorage from '../services/appStorage'
|
|
||||||
|
|
||||||
const BankId = () => (
|
|
||||||
<Image
|
|
||||||
style={themedStyles.icon}
|
|
||||||
source={require('../assets/bankid_low_rgb.png')}
|
|
||||||
accessibilityIgnoresInvertColors
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
const FrejaEid = () => (
|
|
||||||
<Image
|
|
||||||
style={themedStyles.icon}
|
|
||||||
source={require('../assets/freja_eid_logo.png')}
|
|
||||||
accessibilityIgnoresInvertColors
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const Login = () => {
|
|
||||||
const { api } = useApi()
|
|
||||||
const [cancelLoginRequest, setCancelLoginRequest] = useState<
|
|
||||||
(() => Promise<void>) | (() => null)
|
|
||||||
>(() => () => null)
|
|
||||||
const [visible, showModal] = useState(false)
|
|
||||||
const [showLoginMethod, setShowLoginMethod] = useState(false)
|
|
||||||
const [showSchoolPlatformPicker, setShowSchoolPlatformPicker] =
|
|
||||||
useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [loginStatusText, setLoginStatusText] = useState('')
|
|
||||||
const [personalIdNumber, setPersonalIdNumber] = useSettingsStorage(
|
|
||||||
'cachedPersonalIdentityNumber'
|
|
||||||
)
|
|
||||||
const [loginMethodId, setLoginMethodId] = useSettingsStorage('loginMethodId')
|
|
||||||
|
|
||||||
const loginBankIdSameDeviceWithoutId = useFeature(
|
|
||||||
'LOGIN_BANK_ID_SAME_DEVICE_WITHOUT_ID'
|
|
||||||
)
|
|
||||||
const loginWithFrejaEnabled = useFeature('LOGIN_FREJA_EID')
|
|
||||||
const { currentSchoolPlatform, changeSchoolPlatform } = useContext(
|
|
||||||
SchoolPlatformContext
|
|
||||||
)
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
// const t = (key: string) => key;
|
|
||||||
|
|
||||||
const valid = Personnummer.valid(personalIdNumber)
|
|
||||||
|
|
||||||
const loginMethods = [
|
|
||||||
{ id: 'thisdevice', title: t('auth.bankid.OpenOnThisDevice') },
|
|
||||||
{ id: 'otherdevice', title: t('auth.bankid.OpenOnAnotherDevice') },
|
|
||||||
{ id: 'freja', title: t('auth.freja.OpenOnThisDevice') },
|
|
||||||
{ id: 'testuser', title: t('auth.loginAsTestUser') },
|
|
||||||
] as const
|
|
||||||
|
|
||||||
if (loginMethodId === 'freja' && !loginWithFrejaEnabled) {
|
|
||||||
setLoginMethodId('thisdevice')
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loginHandler = async () => {
|
|
||||||
console.debug('Running loginHandler')
|
|
||||||
try {
|
|
||||||
const user = await api.getUser()
|
|
||||||
await AppStorage.clearPersonalData(user)
|
|
||||||
showModal(false)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.on('login', loginHandler)
|
|
||||||
return () => {
|
|
||||||
api.off('login', loginHandler)
|
|
||||||
}
|
|
||||||
}, [api])
|
|
||||||
|
|
||||||
const LoginProviderImage = () => {
|
|
||||||
//if(loginMethodId == 'testuser') return undefined
|
|
||||||
if (loginMethodId === 'freja') {
|
|
||||||
return FrejaEid()
|
|
||||||
}
|
|
||||||
return BankId()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSchoolPlatformName = () => {
|
|
||||||
return schoolPlatforms.find((item) => item.id === currentSchoolPlatform)
|
|
||||||
?.displayName
|
|
||||||
}
|
|
||||||
|
|
||||||
const openBankId = (token: string) => {
|
|
||||||
try {
|
|
||||||
const redirect =
|
|
||||||
loginMethodId === 'thisdevice' ? encodeURIComponent(schema) : ''
|
|
||||||
const bankIdUrl =
|
|
||||||
Platform.OS === 'ios'
|
|
||||||
? `https://app.bankid.com/?autostarttoken=${token}&redirect=${redirect}`
|
|
||||||
: `bankid:///?autostarttoken=${token}&redirect=null`
|
|
||||||
Linking.openURL(bankIdUrl)
|
|
||||||
} catch (err) {
|
|
||||||
setError(t('auth.bankid.OpenManually'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openFreja = (token: string) => {
|
|
||||||
try {
|
|
||||||
const originAppScheme = encodeURIComponent(schema)
|
|
||||||
const frejaUrl =
|
|
||||||
Platform.OS === 'ios'
|
|
||||||
? `${token}&originAppScheme=${originAppScheme}`
|
|
||||||
: `${token}`
|
|
||||||
Linking.openURL(frejaUrl)
|
|
||||||
} catch (err) {
|
|
||||||
setError(t('auth.freja.OpenManually'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUsingPersonalIdNumber =
|
|
||||||
loginMethodId === 'otherdevice' ||
|
|
||||||
(loginMethodId === 'thisdevice' && !loginBankIdSameDeviceWithoutId)
|
|
||||||
|
|
||||||
const startLogin = async (text: string) => {
|
|
||||||
if (loginMethodId === 'freja') {
|
|
||||||
setLoginStatusText(t('auth.freja.Waiting'))
|
|
||||||
showModal(true)
|
|
||||||
const status = await api.loginFreja()
|
|
||||||
setCancelLoginRequest(() => () => status.cancel())
|
|
||||||
openFreja(status.token)
|
|
||||||
status.on('STARTED', () => console.log('Freja eID app not yet opened'))
|
|
||||||
status.on('DELIVERED_TO_MOBILE', () =>
|
|
||||||
console.log('Freja eID app is open')
|
|
||||||
)
|
|
||||||
status.on('CANCELLED', () => {
|
|
||||||
console.log('User pressed cancel in Freja eID')
|
|
||||||
showModal(false)
|
|
||||||
})
|
|
||||||
status.on('APPROVED', () => {
|
|
||||||
console.log('Freja eID ok')
|
|
||||||
setLoginStatusText(t('auth.loginSuccessful'))
|
|
||||||
})
|
|
||||||
} else if (
|
|
||||||
loginMethodId === 'thisdevice' ||
|
|
||||||
loginMethodId === 'otherdevice'
|
|
||||||
) {
|
|
||||||
setLoginStatusText(t('auth.bankid.Waiting'))
|
|
||||||
showModal(true)
|
|
||||||
|
|
||||||
let ssn
|
|
||||||
|
|
||||||
if (isUsingPersonalIdNumber) {
|
|
||||||
ssn = Personnummer.parse(text).format(true)
|
|
||||||
setPersonalIdNumber(ssn)
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = await api.login(ssn)
|
|
||||||
setCancelLoginRequest(() => () => status.cancel())
|
|
||||||
if (status.token !== 'fake' && loginMethodId === 'thisdevice') {
|
|
||||||
openBankId(status.token)
|
|
||||||
}
|
|
||||||
status.on('PENDING', () => console.log('BankID app not yet opened'))
|
|
||||||
status.on('USER_SIGN', () => console.log('BankID app is open'))
|
|
||||||
status.on('CANCELLED', () => {
|
|
||||||
console.log('User pressed cancel in BankID')
|
|
||||||
showModal(false)
|
|
||||||
})
|
|
||||||
status.on('ERROR', () => {
|
|
||||||
setError(t('auth.loginFailed'))
|
|
||||||
showModal(false)
|
|
||||||
})
|
|
||||||
status.on('OK', () => {
|
|
||||||
console.log('BankID ok')
|
|
||||||
setLoginStatusText(t('auth.loginSuccessful'))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await api.login('201212121212')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
|
|
||||||
const currentLoginMethod =
|
|
||||||
loginMethods.find((method) => method.id === loginMethodId) ||
|
|
||||||
loginMethods[0]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View style={styles.loginForm}>
|
|
||||||
{isUsingPersonalIdNumber && (
|
|
||||||
<Input
|
|
||||||
accessible={true}
|
|
||||||
label={t('general.socialSecurityNumber')}
|
|
||||||
autoFocus
|
|
||||||
value={personalIdNumber}
|
|
||||||
style={styles.pnrInput}
|
|
||||||
accessoryLeft={PersonIcon}
|
|
||||||
accessoryRight={(props) => (
|
|
||||||
<TouchableWithoutFeedback
|
|
||||||
accessible={true}
|
|
||||||
onPress={() => setPersonalIdNumber('')}
|
|
||||||
accessibilityHint={t(
|
|
||||||
'login.a11y_clear_social_security_input_field'
|
|
||||||
// {defaultValue: 'Rensa fältet för personnummer'},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CloseOutlineIcon {...props} />
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
)}
|
|
||||||
keyboardType="numeric"
|
|
||||||
onSubmitEditing={(event) => startLogin(event.nativeEvent.text)}
|
|
||||||
caption={error || ''}
|
|
||||||
onChangeText={setPersonalIdNumber}
|
|
||||||
placeholder={t('auth.placeholder_SocialSecurityNumber')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ButtonGroup style={styles.loginButtonGroup} status="primary">
|
|
||||||
<Button
|
|
||||||
accessible={true}
|
|
||||||
onPress={() => startLogin(personalIdNumber)}
|
|
||||||
style={styles.loginButton}
|
|
||||||
appearance="ghost"
|
|
||||||
disabled={isUsingPersonalIdNumber && !valid}
|
|
||||||
status="primary"
|
|
||||||
accessoryLeft={LoginProviderImage}
|
|
||||||
size="medium"
|
|
||||||
>
|
|
||||||
{currentLoginMethod.title}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
accessible={true}
|
|
||||||
onPress={() => {
|
|
||||||
setShowLoginMethod(true)
|
|
||||||
}}
|
|
||||||
style={styles.loginMethodButton}
|
|
||||||
appearance="ghost"
|
|
||||||
status="primary"
|
|
||||||
accessoryLeft={SelectIcon}
|
|
||||||
size="medium"
|
|
||||||
accessibilityHint={t(
|
|
||||||
'login.a11y_select_login_method'
|
|
||||||
// {
|
|
||||||
// defaultValue: 'Välj inloggningsmetod',
|
|
||||||
// }
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
<View style={styles.platformPicker}>
|
|
||||||
<Button
|
|
||||||
appearance="ghost"
|
|
||||||
status="basic"
|
|
||||||
size="small"
|
|
||||||
accessoryRight={SelectIcon}
|
|
||||||
onPress={() => {
|
|
||||||
setShowSchoolPlatformPicker(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getSchoolPlatformName()}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<Modal
|
|
||||||
visible={showLoginMethod}
|
|
||||||
style={styles.modal}
|
|
||||||
onBackdropPress={() => setShowLoginMethod(false)}
|
|
||||||
backdropStyle={styles.backdrop}
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<Text category="h5" style={styles.bankIdLoading}>
|
|
||||||
{t('auth.chooseLoginMethod')}
|
|
||||||
</Text>
|
|
||||||
<List
|
|
||||||
data={
|
|
||||||
loginWithFrejaEnabled
|
|
||||||
? loginMethods
|
|
||||||
: loginMethods.filter((f) => f.id !== 'freja')
|
|
||||||
}
|
|
||||||
ItemSeparatorComponent={Divider}
|
|
||||||
renderItem={({ item }) => (
|
|
||||||
<ListItem
|
|
||||||
title={item.title}
|
|
||||||
accessible={true}
|
|
||||||
accessoryRight={
|
|
||||||
loginMethodId === item.id ? CheckIcon : undefined
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
setLoginMethodId(item.id)
|
|
||||||
setShowLoginMethod(false)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
status="basic"
|
|
||||||
style={styles.cancelButtonStyle}
|
|
||||||
onPress={() => {
|
|
||||||
setShowLoginMethod(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('general.cancel')}
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
</Modal>
|
|
||||||
<Modal
|
|
||||||
visible={visible}
|
|
||||||
style={styles.modal}
|
|
||||||
onBackdropPress={() => showModal(false)}
|
|
||||||
backdropStyle={styles.backdrop}
|
|
||||||
>
|
|
||||||
<Card disabled>
|
|
||||||
<Text style={styles.bankIdLoading}>{loginStatusText}</Text>
|
|
||||||
<Button
|
|
||||||
status="primary"
|
|
||||||
accessible={true}
|
|
||||||
onPress={() => {
|
|
||||||
cancelLoginRequest()
|
|
||||||
showModal(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('general.cancel')}
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
</Modal>
|
|
||||||
<Modal
|
|
||||||
visible={showSchoolPlatformPicker}
|
|
||||||
style={styles.modal}
|
|
||||||
onBackdropPress={() => setShowSchoolPlatformPicker(false)}
|
|
||||||
backdropStyle={styles.backdrop}
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<Text category="h5" style={styles.bankIdLoading}>
|
|
||||||
{t('auth.chooseSchoolPlatform')}
|
|
||||||
</Text>
|
|
||||||
<List
|
|
||||||
data={schoolPlatforms}
|
|
||||||
ItemSeparatorComponent={Divider}
|
|
||||||
renderItem={({ item }) => (
|
|
||||||
<ListItem
|
|
||||||
title={item.displayName}
|
|
||||||
accessible={true}
|
|
||||||
accessoryRight={
|
|
||||||
currentSchoolPlatform === item.id ? CheckIcon : undefined
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
changeSchoolPlatform(item.id)
|
|
||||||
setShowSchoolPlatformPicker(false)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
status="basic"
|
|
||||||
style={styles.cancelButtonStyle}
|
|
||||||
onPress={() => setShowSchoolPlatformPicker(false)}
|
|
||||||
>
|
|
||||||
{t('general.cancel')}
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
backdrop: {
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
},
|
|
||||||
loginForm: {
|
|
||||||
...Layout.mainAxis.flexStart,
|
|
||||||
},
|
|
||||||
pnrInput: { minHeight: 70 },
|
|
||||||
loginButtonGroup: {
|
|
||||||
minHeight: 45,
|
|
||||||
},
|
|
||||||
loginButton: { ...Layout.flex.full },
|
|
||||||
loginMethodButton: { width: 45 },
|
|
||||||
modal: {
|
|
||||||
width: '90%',
|
|
||||||
},
|
|
||||||
bankIdLoading: { margin: 10 },
|
|
||||||
cancelButtonStyle: { marginTop: 15 },
|
|
||||||
icon: {
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
},
|
|
||||||
platformPicker: {
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,61 +0,0 @@
|
||||||
import { Text } from '@ui-kitten/components'
|
|
||||||
import React from 'react'
|
|
||||||
import { Dimensions, Linking, StyleSheet } from 'react-native'
|
|
||||||
import MarkdownBase, {
|
|
||||||
RenderRules,
|
|
||||||
} from '@ronradtke/react-native-markdown-display'
|
|
||||||
import { Sizing } from '../styles'
|
|
||||||
import { Image } from './image.component'
|
|
||||||
|
|
||||||
interface MarkdownProps {
|
|
||||||
children: React.ReactNode
|
|
||||||
style?: StyleSheet.NamedStyles<any>
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules: RenderRules = {
|
|
||||||
image: (node) => {
|
|
||||||
const { src } = node.attributes
|
|
||||||
const url = src.startsWith('/')
|
|
||||||
? `https://elevstockholm.sharepoint.com${src}`
|
|
||||||
: src
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
accessibilityIgnoresInvertColors
|
|
||||||
key={src}
|
|
||||||
src={url}
|
|
||||||
// TODO: Sizing.t5 should not be hardcoded here...
|
|
||||||
// Maybe measure the width from inside the component instead?
|
|
||||||
componentWidth={Dimensions.get('window').width - Sizing.t5 * 2}
|
|
||||||
style={styles.markdownImage}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
link: (node, children, _parent, styles) => {
|
|
||||||
if (children) {
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
key={node.key}
|
|
||||||
style={styles.link}
|
|
||||||
onPress={() => Linking.openURL(node.attributes.href)}
|
|
||||||
>
|
|
||||||
{children.map((child, index) => (
|
|
||||||
<React.Fragment key={index}>{child}</React.Fragment>
|
|
||||||
))}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Markdown = ({ style, children }: MarkdownProps) => {
|
|
||||||
return (
|
|
||||||
<MarkdownBase rules={rules} style={style}>
|
|
||||||
{children}
|
|
||||||
</MarkdownBase>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
markdownImage: { width: '100%', borderRadius: 15 },
|
|
||||||
})
|
|
|
@ -1,86 +0,0 @@
|
||||||
import { MenuItem } from '@skolplattformen/api'
|
|
||||||
import { useMenu } from '../libs/hooks/src'
|
|
||||||
import {
|
|
||||||
Divider,
|
|
||||||
List,
|
|
||||||
StyleService,
|
|
||||||
Text,
|
|
||||||
useStyleSheet,
|
|
||||||
} from '@ui-kitten/components'
|
|
||||||
import 'moment/locale/sv'
|
|
||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
Image,
|
|
||||||
ImageStyle,
|
|
||||||
ListRenderItemInfo,
|
|
||||||
RefreshControl,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import { Layout as LayoutStyle, Sizing, Typography } from '../styles'
|
|
||||||
import { translate } from '../utils/translation'
|
|
||||||
import { useChild } from './childContext.component'
|
|
||||||
import { MenuListItem } from './menuListItem.component'
|
|
||||||
|
|
||||||
// const translate = (key: string) => key;
|
|
||||||
|
|
||||||
export const Menu = () => {
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
const child = useChild()
|
|
||||||
const { data, status, reload } = useMenu(child)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<List
|
|
||||||
contentContainerStyle={styles.contentContainer}
|
|
||||||
data={data}
|
|
||||||
ItemSeparatorComponent={Divider}
|
|
||||||
ListEmptyComponent={
|
|
||||||
<View style={styles.emptyState}>
|
|
||||||
<Text category="h4">{translate('menu.emptyHeadline')}</Text>
|
|
||||||
<Text style={styles.emptyStateDescription}>
|
|
||||||
{translate('menu.emptyText')}
|
|
||||||
</Text>
|
|
||||||
<Image
|
|
||||||
accessibilityIgnoresInvertColors={false}
|
|
||||||
source={require('../assets/children.png')}
|
|
||||||
style={styles.emptyStateImage as ImageStyle}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
renderItem={({ item }: ListRenderItemInfo<MenuItem>) => (
|
|
||||||
<MenuListItem key={item.title} item={item} />
|
|
||||||
)}
|
|
||||||
style={styles.container}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl refreshing={status === 'loading'} onRefresh={reload} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
container: {
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
padding: Sizing.t3,
|
|
||||||
},
|
|
||||||
contentContainer: {
|
|
||||||
paddingHorizontal: Sizing.t5,
|
|
||||||
paddingVertical: Sizing.t2,
|
|
||||||
backgroundColor: 'background-basic-color-1',
|
|
||||||
borderRadius: 25,
|
|
||||||
},
|
|
||||||
emptyState: {
|
|
||||||
...LayoutStyle.center,
|
|
||||||
...LayoutStyle.flex.full,
|
|
||||||
},
|
|
||||||
emptyStateDescription: {
|
|
||||||
...Typography.align.center,
|
|
||||||
lineHeight: 21,
|
|
||||||
paddingHorizontal: Sizing.t3,
|
|
||||||
marginTop: Sizing.t3,
|
|
||||||
},
|
|
||||||
emptyStateImage: {
|
|
||||||
...Sizing.aspectRatio(0.8),
|
|
||||||
marginTop: 50,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { MenuItem } from '@skolplattformen/api'
|
|
||||||
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
|
|
||||||
import React from 'react'
|
|
||||||
import { View } from 'react-native'
|
|
||||||
import { Sizing, Typography } from '../styles'
|
|
||||||
|
|
||||||
interface MenuListItemProps {
|
|
||||||
item: MenuItem
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MenuListItem = ({ item }: MenuListItemProps) => {
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text style={styles.title}>{item.title}</Text>
|
|
||||||
<Text category="p1">{item.description}</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
container: {
|
|
||||||
width: '100%',
|
|
||||||
paddingVertical: Sizing.t3,
|
|
||||||
},
|
|
||||||
topContainer: {
|
|
||||||
margin: Sizing.t1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
...Typography.header,
|
|
||||||
marginBottom: Sizing.t1,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,130 +0,0 @@
|
||||||
import { useApi } from '../libs/hooks/src'
|
|
||||||
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { Linking, Modal, TouchableOpacity, View } from 'react-native'
|
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
||||||
import { WebView } from 'react-native-webview'
|
|
||||||
import { Layout, Sizing } from '../styles'
|
|
||||||
import { BackIcon, ExternalLinkIcon } from './icon.component'
|
|
||||||
|
|
||||||
interface ModalWebViewProps {
|
|
||||||
url: string
|
|
||||||
sharedCookiesEnabled: boolean
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
export const ModalWebView = ({
|
|
||||||
url,
|
|
||||||
onClose,
|
|
||||||
sharedCookiesEnabled,
|
|
||||||
}: ModalWebViewProps) => {
|
|
||||||
const [modalVisible, setModalVisible] = React.useState(true)
|
|
||||||
const { api } = useApi()
|
|
||||||
const [title, setTitle] = React.useState('...')
|
|
||||||
const [headers, setHeaders] = useState<{ [index: string]: string }>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getHeaders = async (urlToGetSessionFor: string) => {
|
|
||||||
if (sharedCookiesEnabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const newHeaders = await api.getSessionHeaders(urlToGetSessionFor)
|
|
||||||
setHeaders(newHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
getHeaders(url)
|
|
||||||
}, [url, sharedCookiesEnabled, api])
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
setModalVisible(false)
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
const openInApp = () => {
|
|
||||||
Linking.openURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = useStyleSheet(themedStyles)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
animationType="slide"
|
|
||||||
statusBarTranslucent={true}
|
|
||||||
visible={modalVisible}
|
|
||||||
onRequestClose={closeModal}
|
|
||||||
>
|
|
||||||
<SafeAreaView style={styles.container}>
|
|
||||||
<View style={styles.headerWrapper}>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<TouchableOpacity onPress={closeModal}>
|
|
||||||
<BackIcon
|
|
||||||
style={styles.backIcon}
|
|
||||||
fill={styles.backIcon.shadowColor}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<Text category="s1" style={styles.headerText} numberOfLines={1}>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity onPress={openInApp}>
|
|
||||||
<ExternalLinkIcon
|
|
||||||
style={styles.shareIcon}
|
|
||||||
fill={styles.shareIcon.shadowColor}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{(headers || sharedCookiesEnabled) && (
|
|
||||||
<WebView
|
|
||||||
style={styles.webview}
|
|
||||||
source={{ uri: url, headers }}
|
|
||||||
sharedCookiesEnabled={sharedCookiesEnabled}
|
|
||||||
thirdPartyCookiesEnabled={sharedCookiesEnabled}
|
|
||||||
onLoad={(event) => {
|
|
||||||
setTitle(event.nativeEvent.title)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SafeAreaView>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themedStyles = StyleService.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: 'background-basic-color-2',
|
|
||||||
},
|
|
||||||
headerWrapper: {
|
|
||||||
marginTop: Sizing.t1,
|
|
||||||
padding: Sizing.t1,
|
|
||||||
borderRadius: 2,
|
|
||||||
borderColor: 'basic-color-200',
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
backgroundColor: 'background-basic-color-2',
|
|
||||||
},
|
|
||||||
backdrop: {
|
|
||||||
backgroundColor: 'color-basic-transparent-600',
|
|
||||||
},
|
|
||||||
headerText: {
|
|
||||||
overflow: 'hidden',
|
|
||||||
width: '85%',
|
|
||||||
paddingRight: 2,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
...Layout.flex.row,
|
|
||||||
...Layout.mainAxis.center,
|
|
||||||
paddingHorizontal: Sizing.t3,
|
|
||||||
paddingVertical: Sizing.t1,
|
|
||||||
backgroundColor: 'background-basic-color-2',
|
|
||||||
},
|
|
||||||
shareIcon: {
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
shadowColor: 'color-basic-600',
|
|
||||||
},
|
|
||||||
backIcon: {
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
marginRight: Sizing.t4,
|
|
||||||
shadowColor: 'color-basic-600',
|
|
||||||
},
|
|
||||||
webview: {},
|
|
||||||
})
|
|
|
@ -1,193 +0,0 @@
|
||||||
import { NavigationContainer } from '@react-navigation/native'
|
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
|
||||||
import {
|
|
||||||
Child as ChildType,
|
|
||||||
NewsItem as NewsItemType,
|
|
||||||
} from '@skolplattformen/api'
|
|
||||||
import { useApi } from '../libs/hooks/src'
|
|
||||||
import { useTheme } from '@ui-kitten/components'
|
|
||||||
import { Library } from 'libraries.json'
|
|
||||||
import React, { useEffect } from 'react'
|
|
||||||
import { StatusBar, useColorScheme } from 'react-native'
|
|
||||||
import { schema } from '../app.json'
|
|
||||||
import {
|
|
||||||
darkNavigationTheme,
|
|
||||||
lightNavigationTheme,
|
|
||||||
} from '../design/navigationThemes'
|
|
||||||
import { useAppState } from '../hooks/useAppState'
|
|
||||||
import { useLangCode } from '../hooks/useLangCode'
|
|
||||||
import useSettingsStorage, {
|
|
||||||
initializeSettingsState,
|
|
||||||
} from '../hooks/useSettingsStorage'
|
|
||||||
import { isRTL } from '../services/languageService'
|
|
||||||
import Absence, { absenceRouteOptions } from './absence.component'
|
|
||||||
import { Auth, authRouteOptions } from './auth.component'
|
|
||||||
import { Child, childRouteOptions } from './child.component'
|
|
||||||
import { childenRouteOptions, Children } from './children.component'
|
|
||||||
import { libraryRouteOptions, LibraryScreen } from './library.component'
|
|
||||||
import { NewsItem, newsItemRouteOptions } from './newsItem.component'
|
|
||||||
import { SetLanguage, setLanguageRouteOptions } from './setLanguage.component'
|
|
||||||
import { settingsRouteOptions, SettingsScreen } from './settings.component'
|
|
||||||
import {
|
|
||||||
settingsAppearanceRouteOptions,
|
|
||||||
SettingsAppearanceScreen,
|
|
||||||
} from './settingsAppearance.component'
|
|
||||||
import {
|
|
||||||
settingsAppearanceThemeRouteOptions,
|
|
||||||
SettingsAppearanceThemeScreen,
|
|
||||||
} from './settingsAppearanceTheme.component'
|
|
||||||
import {
|
|
||||||
settingsLicensesRouteOptions,
|
|
||||||
SettingsLicensesScreen,
|
|
||||||
} from './settingsLicenses.component'
|
|
||||||
|
|
||||||
export type RootStackParamList = {
|
|
||||||
Login: undefined
|
|
||||||
IsLoggedIn: undefined
|
|
||||||
Children: undefined
|
|
||||||
Settings: { rand?: number } | undefined
|
|
||||||
SettingsAppearance: undefined
|
|
||||||
SettingsAppearanceTheme: undefined
|
|
||||||
SettingsLicenses: undefined
|
|
||||||
Library: {
|
|
||||||
library: Library
|
|
||||||
}
|
|
||||||
Child: {
|
|
||||||
child: ChildType
|
|
||||||
color: string
|
|
||||||
initialRouteName?: string
|
|
||||||
}
|
|
||||||
NewsItem: { newsItem: NewsItemType; child: ChildType }
|
|
||||||
Absence: { child: ChildType }
|
|
||||||
SetLanguage: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const { Navigator, Screen } = createNativeStackNavigator<RootStackParamList>()
|
|
||||||
|
|
||||||
const linking = {
|
|
||||||
prefixes: [schema],
|
|
||||||
config: {
|
|
||||||
screens: {
|
|
||||||
Login: 'login',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppNavigator = () => {
|
|
||||||
const { isLoggedIn, api } = useApi()
|
|
||||||
|
|
||||||
const [usingSystemTheme] = useSettingsStorage('usingSystemTheme')
|
|
||||||
const [theme] = useSettingsStorage('theme')
|
|
||||||
const systemTheme = useColorScheme()
|
|
||||||
const colorScheme = usingSystemTheme ? systemTheme : theme
|
|
||||||
const langCode = useLangCode()
|
|
||||||
|
|
||||||
const colors = useTheme()
|
|
||||||
|
|
||||||
const currentAppState = useAppState()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
initializeSettingsState()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkUser = async () => {
|
|
||||||
if (currentAppState === 'active' && isLoggedIn) {
|
|
||||||
const { isAuthenticated } = await api.getUser()
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
await api.logout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkUser()
|
|
||||||
}, [currentAppState, isLoggedIn, api])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavigationContainer
|
|
||||||
linking={linking}
|
|
||||||
theme={
|
|
||||||
colorScheme === 'dark' ? darkNavigationTheme : lightNavigationTheme
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StatusBar />
|
|
||||||
|
|
||||||
<Navigator
|
|
||||||
screenOptions={() => ({
|
|
||||||
headerLargeTitle: false,
|
|
||||||
headerLargeTitleHideShadow: true,
|
|
||||||
direction: isRTL(langCode) ? 'rtl' : 'ltr',
|
|
||||||
headerStyle: {
|
|
||||||
backgroundColor:
|
|
||||||
colorScheme === 'dark'
|
|
||||||
? colors['background-basic-color-2']
|
|
||||||
: colors['background-basic-color-1'],
|
|
||||||
},
|
|
||||||
headerLargeStyle: {
|
|
||||||
backgroundColor: colors['background-basic-color-2'],
|
|
||||||
},
|
|
||||||
headerLargeTitleStyle: {
|
|
||||||
fontFamily: 'Poppins-ExtraBold',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{isLoggedIn ? (
|
|
||||||
<>
|
|
||||||
<Screen
|
|
||||||
name="Children"
|
|
||||||
component={Children}
|
|
||||||
options={childenRouteOptions(colorScheme === 'dark')}
|
|
||||||
/>
|
|
||||||
<Screen
|
|
||||||
name="Child"
|
|
||||||
component={Child}
|
|
||||||
options={childRouteOptions(colorScheme === 'dark')}
|
|
||||||
/>
|
|
||||||
<Screen
|
|
||||||
name="NewsItem"
|
|
||||||
component={NewsItem}
|
|
||||||
options={newsItemRouteOptions(colorScheme === 'dark')}
|
|
||||||
/>
|
|
||||||
<Screen
|
|
||||||
name="Absence"
|
|
||||||
component={Absence}
|
|
||||||
options={absenceRouteOptions(colorScheme === 'dark')}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Screen name="Login" component={Auth} options={authRouteOptions} />
|
|
||||||
)}
|
|
||||||
<Screen
|
|
||||||
name="SetLanguage"
|
|
||||||
component={SetLanguage}
|
|
||||||
options={setLanguageRouteOptions}
|
|
||||||
/>
|
|
||||||
<Screen
|
|
||||||
name="Settings"
|
|
||||||
component={SettingsScreen}
|
|
||||||
options={settingsRouteOptions}
|
|
||||||
/>
|
|
||||||
<Screen
|
|
||||||
name="SettingsAppearance"
|
|
||||||
component={SettingsAppearanceScreen}
|
|
||||||
options={settingsAppearanceRouteOptions}
|
|
||||||
/>
|
|
||||||
<Screen
|
|
||||||
name="SettingsAppearanceTheme"
|
|
||||||
component={SettingsAppearanceThemeScreen}
|
|
||||||
options={settingsAppearanceThemeRouteOptions}
|
|
||||||
/>
|
|
||||||
<Screen
|
|
||||||
name="SettingsLicenses"
|
|
||||||
component={SettingsLicensesScreen}
|
|
||||||
options={settingsLicensesRouteOptions}
|
|
||||||
/>
|
|
||||||
<Screen
|
|
||||||
name="Library"
|
|
||||||
component={LibraryScreen}
|
|
||||||
options={libraryRouteOptions}
|
|
||||||
/>
|
|
||||||
</Navigator>
|
|
||||||
</NavigationContainer>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { Text } from '@ui-kitten/components'
|
|
||||||
import React from 'react'
|
|
||||||
import { StyleSheet, View } from 'react-native'
|
|
||||||
import { Layout } from '../styles'
|
|
||||||
import { fontSize } from '../styles/typography'
|
|
||||||
|
|
||||||
interface NavigationTitleProps {
|
|
||||||
title?: string
|
|
||||||
subtitle?: string
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Navigation Title with a smaller subtitle.
|
|
||||||
*/
|
|
||||||
export const NavigationTitle = ({ title, subtitle }: NavigationTitleProps) => {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
{title && (
|
|
||||||
<Text style={styles.title} numberOfLines={1} ellipsizeMode="tail">
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{subtitle && (
|
|
||||||
<Text style={styles.subtitle}>
|
|
||||||
{subtitle.substring(0, subtitle.indexOf(' '))}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
...Layout.center,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
...fontSize.sm,
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
subtitle: { ...fontSize.base },
|
|
||||||
})
|
|