Compare commits
1 Commits
master
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
|
7671cb863a |
|
@ -1,5 +0,0 @@
|
|||
API_URI=http://localhost:8080/graphql
|
||||
RAW_API_URL=localhost
|
||||
WEBSITE=http://localhost:3000
|
||||
CONTACT_EMAIL=contact@localhost
|
||||
CDN_URL=http://localhost:9000
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -29,7 +29,6 @@ release/
|
|||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
|
||||
# node.js
|
||||
#
|
||||
|
@ -63,7 +62,3 @@ buck-out/
|
|||
remote-schema.graphql
|
||||
upload.jks
|
||||
google-services.json
|
||||
|
||||
sentry.properties
|
||||
|
||||
.env.production
|
||||
|
|
26
README.md
26
README.md
|
@ -2,6 +2,32 @@
|
|||
|
||||
![Screenshot](/screenshots/homescreen.jpg?raw=true)
|
||||
|
||||
A mobile app for practising the theoretical part of the [vocational exam](https://cke.gov.pl/en/vocational-examination/).
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Node.JS
|
||||
2. yarn
|
||||
3. [JDK and Android Studio](https://reactnative.dev/docs/environment-setup)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone this repo
|
||||
|
||||
```
|
||||
git clone git@github.com:zdam-egzamin-zawodowy/mobile-app.git
|
||||
```
|
||||
|
||||
2. Open the folder with this project in a terminal.
|
||||
3. `yarn install`
|
||||
4. `yarn run android` (iOS isn't supported)
|
||||
|
||||
### Firebase (Analytics and Crashlytics)
|
||||
|
||||
Create a new firebase project and add the app `com.dawidwysokinski.zdamegzaminzawodowy`, then download the `google-services.json` file and place it at the following location: `/android/app/google-services.json`.
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the MIT License. See `LICENSE` for more information.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
apply plugin: "com.android.application"
|
||||
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
|
||||
apply plugin: "com.google.gms.google-services"
|
||||
apply plugin: "com.google.firebase.crashlytics"
|
||||
|
||||
import com.android.build.OutputFile
|
||||
|
||||
|
@ -83,7 +84,6 @@ project.ext.react = [
|
|||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
apply from: "../../node_modules/@sentry/react-native/sentry.gradle"
|
||||
|
||||
/**
|
||||
* Set this to true to create two separate APKs instead of one:
|
||||
|
@ -93,7 +93,7 @@ apply from: "../../node_modules/@sentry/react-native/sentry.gradle"
|
|||
* Upload all the APKs to the Play Store and people will download
|
||||
* the correct one based on the CPU architecture of their device.
|
||||
*/
|
||||
def enableSeparateBuildPerCPUArchitecture = false
|
||||
def enableSeparateBuildPerCPUArchitecture = true
|
||||
|
||||
/**
|
||||
* Run Proguard to shrink the Java bytecode in release builds.
|
||||
|
@ -122,11 +122,6 @@ def jscFlavor = 'org.webkit:android-jsc:+'
|
|||
*/
|
||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
|
||||
/**
|
||||
* Architectures to build native code for in debug.
|
||||
*/
|
||||
def nativeArchitectures = project.getProperties().get("reactNativeDebugArchitectures")
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
|
@ -141,8 +136,8 @@ android {
|
|||
applicationId "com.dawidwysokinski.zdamegzaminzawodowy"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10000
|
||||
versionName "2.0.9"
|
||||
versionCode 5
|
||||
versionName "2.0.5"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
|
@ -171,11 +166,6 @@ android {
|
|||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
if (nativeArchitectures) {
|
||||
ndk {
|
||||
abiFilters nativeArchitectures.split(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
|
@ -233,7 +223,7 @@ dependencies {
|
|||
// Run this once to be able to run the application with BUCK
|
||||
// puts all compile dependencies into folder libs for BUCK to use
|
||||
task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.implementation
|
||||
from configurations.compile
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:textColor">#000000</item>
|
||||
</style>
|
||||
|
||||
<style name="BootTheme" parent="AppTheme">
|
||||
|
|
|
@ -2,18 +2,20 @@
|
|||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "30.0.2"
|
||||
buildToolsVersion = "29.0.3"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
ndkVersion = "21.4.7075529"
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
ndkVersion = "20.1.5948944"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:4.2.2")
|
||||
classpath("com.android.tools.build:gradle:4.1.0")
|
||||
classpath("com.google.gms:google-services:4.3.4")
|
||||
classpath ("com.google.firebase:firebase-crashlytics-gradle:2.5.1")
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
|
@ -21,7 +23,6 @@ buildscript {
|
|||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
|
|
|
@ -25,4 +25,4 @@ android.useAndroidX=true
|
|||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.99.0
|
||||
FLIPPER_VERSION=0.75.1
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
7
firebase.json
Normal file
7
firebase.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"react-native": {
|
||||
"crashlytics_debug_enabled": true,
|
||||
"crashlytics_is_error_generation_on_js_crash_enabled": true,
|
||||
"crashlytics_ndk_enabled": true
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
require_relative '../node_modules/react-native/scripts/react_native_pods'
|
||||
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
|
||||
|
||||
platform :ios, '11.0'
|
||||
platform :ios, '10.0'
|
||||
|
||||
target 'ZdamEgzaminZawodowy' do
|
||||
config = use_native_modules!
|
||||
|
@ -25,6 +25,5 @@ target 'ZdamEgzaminZawodowy' do
|
|||
|
||||
post_install do |installer|
|
||||
react_native_post_install(installer)
|
||||
__apply_Xcode_12_5_M1_post_install_workaround(installer)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -154,7 +154,6 @@
|
|||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
9A7A0868A2864CFB852897D6 /* Upload Debug Symbols to Sentry */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -233,7 +232,7 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "export SENTRY_PROPERTIES=sentry.properties\nexport EXTRA_PACKAGER_ARGS=\"--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map\"\nset -e\n\nexport NODE_BINARY=node\n../node_modules/@sentry/cli/bin/sentry-cli react-native xcode ../node_modules/react-native/scripts/react-native-xcode.sh\n";
|
||||
shellScript = "set -e\n\nexport NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n";
|
||||
};
|
||||
FD10A7F022414F080027D42C /* Start Packager */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
|
@ -254,20 +253,6 @@
|
|||
shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9A7A0868A2864CFB852897D6 /* Upload Debug Symbols to Sentry */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Upload Debug Symbols to Sentry";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "export SENTRY_PROPERTIES=sentry.properties\n../node_modules/@sentry/cli/bin/sentry-cli upload-dsym";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
|
37
package.json
37
package.json
|
@ -1,38 +1,33 @@
|
|||
{
|
||||
"name": "zdam-egzamin-zawodowy",
|
||||
"version": "2.0.9",
|
||||
"name": "ZdamEgzaminZawodowy",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"android": "ENVFILE=.env.development react-native run-android",
|
||||
"android:release": "ENVFILE=.env.production react-native run-android --variant=release",
|
||||
"android:bundle-release": "cd android && ENVFILE=.env.production ./gradlew bundleRelease && cd ..",
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"start": "react-native start",
|
||||
"test": "jest",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"codegen": "graphql-codegen",
|
||||
"adb": "adb reverse tcp:8080 tcp:8080 && adb reverse tcp:3000 tcp:3000"
|
||||
"codegen": "graphql-codegen"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.3.13",
|
||||
"@react-native-async-storage/async-storage": "^1.15.1",
|
||||
"@react-native-community/masked-view": "^0.1.10",
|
||||
"@react-native-firebase/analytics": "^12.0.0",
|
||||
"@react-native-firebase/app": "^12.0.0",
|
||||
"@react-native-firebase/crashlytics": "^12.0.0",
|
||||
"@react-native-picker/picker": "^1.14.0",
|
||||
"@react-navigation/native": "^5.9.4",
|
||||
"@react-navigation/stack": "^5.14.4",
|
||||
"@sentry/react-native": "^3.2.3",
|
||||
"date-fns": "^2.19.0",
|
||||
"graphql": "^15.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"native-base": "^2.15.2",
|
||||
"patch-package": "^6.4.7",
|
||||
"polish-plurals": "^1.1.0",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"react": "17.0.2",
|
||||
"react-native": "0.66.1",
|
||||
"react": "17.0.1",
|
||||
"react-native": "0.64.0",
|
||||
"react-native-bootsplash": "^3.2.0",
|
||||
"react-native-config": "^1.4.5",
|
||||
"react-native-gesture-handler": "^1.10.3",
|
||||
"react-native-reanimated": "^2.1.0",
|
||||
"react-native-safe-area-context": "^3.2.0",
|
||||
|
@ -44,22 +39,22 @@
|
|||
"devDependencies": {
|
||||
"@babel/core": "^7.12.9",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@graphql-codegen/cli": "^2.0.0",
|
||||
"@graphql-codegen/typescript": "^2.0.0",
|
||||
"@graphql-codegen/typescript-operations": "^2.0.0",
|
||||
"@graphql-codegen/cli": "^1.21.3",
|
||||
"@graphql-codegen/typescript": "^1.21.1",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.15",
|
||||
"@react-native-community/eslint-config": "^2.0.0",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/react-native": "^0.66.1",
|
||||
"@types/react-test-renderer": "^17.0.1",
|
||||
"@types/react-native": "^0.64.0",
|
||||
"@types/react-test-renderer": "^16.9.2",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"eslint": "^7.14.0",
|
||||
"jest": "^26.6.3",
|
||||
"metro-react-native-babel-preset": "^0.66.2",
|
||||
"metro-react-native-babel-preset": "^0.64.0",
|
||||
"prettier": "^2.2.1",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"react-test-renderer": "17.0.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"resolutions": {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,33 +1,32 @@
|
|||
import React from 'react';
|
||||
import Config from 'react-native-config';
|
||||
import { EMAIL, WEBSITE } from 'config/app';
|
||||
import buildURL from 'utils/buildURL';
|
||||
|
||||
import { Linking, StyleSheet } from 'react-native';
|
||||
import { ActionSheet, Icon, NativeBase } from 'native-base';
|
||||
|
||||
const OPTIONS = ['Strona internetowa', 'Kontakt', 'Anuluj'];
|
||||
enum OptionIndex {
|
||||
WEBSITE,
|
||||
CONTACT,
|
||||
CANCEL,
|
||||
}
|
||||
export interface MenuProps
|
||||
extends Omit<NativeBase.Icon, 'onPress' | 'type' | 'name'> {}
|
||||
|
||||
export type MenuProps = Omit<NativeBase.Icon, 'onPress' | 'type' | 'name'>;
|
||||
const OPTIONS = ['Strona internetowa', 'Kontakt', 'Anuluj'];
|
||||
const WEBSITE_OPT_INDEX = 0;
|
||||
const CONTACT_OPT_INDEX = 1;
|
||||
const CANCEL_OPT_INDEX = OPTIONS.length - 1;
|
||||
|
||||
const Menu = ({ style, ...rest }: MenuProps) => {
|
||||
const showMenu = () => {
|
||||
ActionSheet.show(
|
||||
{
|
||||
options: OPTIONS,
|
||||
cancelButtonIndex: OptionIndex.CANCEL,
|
||||
cancelButtonIndex: CANCEL_OPT_INDEX,
|
||||
},
|
||||
index => {
|
||||
switch (index) {
|
||||
case OptionIndex.WEBSITE:
|
||||
Linking.openURL(Config.WEBSITE);
|
||||
case WEBSITE_OPT_INDEX:
|
||||
Linking.openURL(WEBSITE);
|
||||
break;
|
||||
case OptionIndex.CONTACT:
|
||||
Linking.openURL(buildURL('email', Config.CONTACT_EMAIL));
|
||||
case CONTACT_OPT_INDEX:
|
||||
Linking.openURL(buildURL('email', EMAIL));
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
|
7
src/config/analytics.ts
Normal file
7
src/config/analytics.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export enum Event {
|
||||
SaveQualification = 'save_qualification',
|
||||
UnSaveQualification = 'unsave_qualification',
|
||||
StartTest = 'start_test',
|
||||
FinishTest = 'finish_test',
|
||||
SelectAnswer = 'select_answer',
|
||||
}
|
3
src/config/api.ts
Normal file
3
src/config/api.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const API_URI = __DEV__
|
||||
? 'http://localhost:8080/graphql'
|
||||
: 'https://api.zdamegzaminzawodowy.pl/graphql';
|
|
@ -1 +1,7 @@
|
|||
export const WEBSITE = __DEV__
|
||||
? 'http://localhost:3000'
|
||||
: 'https://zdamegzaminzawodowy.pl';
|
||||
|
||||
export const EMAIL = 'kontakt@zdamegzaminzawodowy.pl';
|
||||
|
||||
export const QUESTIONS = [1, 40];
|
||||
|
|
7
src/config/cdn.ts
Normal file
7
src/config/cdn.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { WEBSITE } from './app';
|
||||
|
||||
export const CDN_URI = __DEV__
|
||||
? 'http://localhost:9000/'
|
||||
: 'https://cdn.zdamegzaminzawodowy.pl/';
|
||||
|
||||
export const IMAGE_RESIZING_SERVICE = `${WEBSITE}/_next/image`;
|
|
@ -1,6 +1,6 @@
|
|||
export enum Screen {
|
||||
HOME = 'HomeScreen',
|
||||
TEST = 'TestScreen',
|
||||
Home = 'HomeScreen',
|
||||
Test = 'TestScreen',
|
||||
}
|
||||
|
||||
export type TestScreenParams = {
|
||||
|
@ -9,6 +9,6 @@ export type TestScreenParams = {
|
|||
};
|
||||
|
||||
export type AppStackParamList = {
|
||||
[Screen.HOME]: undefined;
|
||||
[Screen.TEST]: TestScreenParams;
|
||||
[Screen.Home]: undefined;
|
||||
[Screen.Test]: TestScreenParams;
|
||||
};
|
||||
|
|
|
@ -15,20 +15,17 @@ export const createClient = (
|
|||
cache: new InMemoryCache(),
|
||||
link: ApolloLink.from([
|
||||
onError(({ graphQLErrors, networkError }) => {
|
||||
if (!__DEV__) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (graphQLErrors) {
|
||||
graphQLErrors.forEach(({ message, locations, path }) =>
|
||||
console.log(
|
||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (networkError) {
|
||||
console.log(`[Network error]: ${networkError}`);
|
||||
if (__DEV__) {
|
||||
if (graphQLErrors) {
|
||||
graphQLErrors.forEach(({ message, locations, path }) =>
|
||||
console.log(
|
||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (networkError) {
|
||||
console.log(`[Network error]: ${networkError}`);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new HttpLink({
|
||||
|
|
|
@ -66,7 +66,7 @@ export default (variables /* : * */ = variable) => {
|
|||
},
|
||||
},
|
||||
backgroundColor:
|
||||
Platform.OS === OS.ANDROID ? variables.footerDefaultBg : undefined,
|
||||
Platform.OS === OS.Android ? variables.footerDefaultBg : undefined,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
flex: 1,
|
||||
|
|
|
@ -47,9 +47,9 @@ export default (variables /* : * */ = variable) => {
|
|||
shadowOffset: null,
|
||||
shadowRadius: null,
|
||||
shadowOpacity: null,
|
||||
paddingTop: platform === OS.ANDROID ? StatusBar.currentHeight : undefined,
|
||||
paddingTop: platform === OS.Android ? StatusBar.currentHeight : undefined,
|
||||
height:
|
||||
platform === OS.ANDROID
|
||||
platform === OS.Android
|
||||
? variables.toolbarHeight + (StatusBar.currentHeight ?? 0)
|
||||
: variables.toolbarHeight,
|
||||
},
|
||||
|
|
|
@ -97,7 +97,7 @@ export default (variables /* : * */ = variable) => {
|
|||
paddingTop:
|
||||
platform === OS.IOS ? variables.listItemPadding + 25 : undefined,
|
||||
paddingBottom:
|
||||
platform === OS.ANDROID ? variables.listItemPadding + 20 : undefined,
|
||||
platform === OS.Android ? variables.listItemPadding + 20 : undefined,
|
||||
flexDirection: 'row',
|
||||
borderColor: variables.listBorderColor,
|
||||
'NativeBase.Text': {
|
||||
|
|
|
@ -12,8 +12,8 @@ export default (variables /* : * */ = variable) => {
|
|||
justifyContent: 'center',
|
||||
'.scrollable': {
|
||||
paddingHorizontal: 20,
|
||||
flex: platform === OS.ANDROID ? 0 : 1,
|
||||
minWidth: platform === OS.ANDROID ? undefined : 60,
|
||||
flex: platform === OS.Android ? 0 : 1,
|
||||
minWidth: platform === OS.Android ? undefined : 60,
|
||||
},
|
||||
'NativeBase.Text': {
|
||||
color: variables.topTabBarTextColor,
|
||||
|
|
|
@ -211,6 +211,6 @@ export type Variables = {
|
|||
};
|
||||
|
||||
export enum OS {
|
||||
ANDROID = 'android',
|
||||
Android = 'android',
|
||||
IOS = 'ios',
|
||||
}
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import React, { PropsWithChildren, useCallback, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useEffectOnce, useUpdateEffect } from 'react-use';
|
||||
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
|
||||
import analytics from '@react-native-firebase/analytics';
|
||||
import { context as Context } from './context';
|
||||
import { Event } from 'config/analytics';
|
||||
|
||||
export interface SavedQualificationsProviderProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ASYNC_STORAGE_KEY = 'saved_qualifications';
|
||||
|
||||
export const SavedQualificationsProvider = ({
|
||||
children,
|
||||
}: PropsWithChildren<{}>) => {
|
||||
}: SavedQualificationsProviderProps) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [savedQualifications, setSavedQualifications] = useState<number[]>([]);
|
||||
const asyncStorage = useAsyncStorage(ASYNC_STORAGE_KEY);
|
||||
|
@ -46,6 +52,13 @@ export const SavedQualificationsProvider = ({
|
|||
? ids => [...ids, id]
|
||||
: ids => ids.filter(otherID => otherID !== id),
|
||||
);
|
||||
|
||||
analytics().logEvent(
|
||||
save ? Event.SaveQualification : Event.UnSaveQualification,
|
||||
{
|
||||
id: id.toString(),
|
||||
},
|
||||
);
|
||||
},
|
||||
[setSavedQualifications],
|
||||
);
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import * as Sentry from '@sentry/react-native';
|
||||
import Config from 'react-native-config';
|
||||
|
||||
export const routingInstrumentation = new Sentry.ReactNavigationInstrumentation();
|
||||
|
||||
const initSentry = () => {
|
||||
if (!Config.SENTRY_DSN) {
|
||||
return;
|
||||
}
|
||||
|
||||
Sentry.init({
|
||||
dsn: Config.SENTRY_DSN,
|
||||
environment: __DEV__ ? 'development' : 'production',
|
||||
integrations: [
|
||||
new Sentry.ReactNativeTracing({
|
||||
tracingOrigins: [Config.RAW_API_URL],
|
||||
routingInstrumentation,
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: 0.3,
|
||||
release: __DEV__
|
||||
? 'com.dawidwysokinski.zdamegzaminzawodowy@development'
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
export default initSentry;
|
|
@ -1,17 +1,13 @@
|
|||
import 'react-native-gesture-handler';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import * as Sentry from '@sentry/react-native';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import RNBootSplash from 'react-native-bootsplash';
|
||||
import { Root, StyleProvider } from 'native-base';
|
||||
import { createClient } from 'libs/graphql';
|
||||
import Config from 'react-native-config';
|
||||
import { API_URI } from 'config/api';
|
||||
import Navigation from './Navigation';
|
||||
import { createTheme, variables } from '../libs/native-base';
|
||||
import { SavedQualificationsProvider } from '../libs/savedqualifications';
|
||||
import initSentry from '../libs/sentry/initSentry';
|
||||
|
||||
initSentry();
|
||||
|
||||
const BaseApp = () => {
|
||||
useEffect(() => {
|
||||
|
@ -22,12 +18,8 @@ const BaseApp = () => {
|
|||
};
|
||||
|
||||
const App = () => {
|
||||
const theme = useMemo(() => {
|
||||
return createTheme(variables);
|
||||
}, []);
|
||||
const client = useMemo(() => {
|
||||
return createClient(Config.API_URI);
|
||||
}, []);
|
||||
const theme = useRef(createTheme(variables)).current;
|
||||
const client = useRef(createClient(API_URI)).current;
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
|
@ -42,4 +34,4 @@ const App = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Sentry.wrap(App);
|
||||
export default App;
|
||||
|
|
|
@ -7,7 +7,7 @@ import Professions from './components/Professions/Professions';
|
|||
|
||||
const HomeScreen = () => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [mode, setMode] = useState(Mode.ALL);
|
||||
const [mode, setMode] = useState(Mode.All);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
|
|
@ -5,9 +5,9 @@ import { Keyboard, StyleSheet, TextInput } from 'react-native';
|
|||
import { Icon, Input, Item, Header as NBHeader, View } from 'native-base';
|
||||
import Menu from 'common/Menu/Menu';
|
||||
|
||||
export type HeaderProps = {
|
||||
export interface HeaderProps {
|
||||
onSearch?: (search: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const Header = ({ onSearch }: HeaderProps) => {
|
||||
const inputRef = useRef<Input>(null);
|
||||
|
|
|
@ -2,28 +2,28 @@ import React from 'react';
|
|||
import { Button, Segment, Text } from 'native-base';
|
||||
|
||||
export enum Mode {
|
||||
ALL,
|
||||
SAVED,
|
||||
All,
|
||||
Saved,
|
||||
}
|
||||
|
||||
export type ModeSelectorProps = {
|
||||
export interface ModeSelectorProps {
|
||||
mode: Mode;
|
||||
onChangeMode: (mode: Mode) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const ModeSelector = ({ mode, onChangeMode }: ModeSelectorProps) => {
|
||||
return (
|
||||
<Segment>
|
||||
<Button
|
||||
first
|
||||
onPress={() => onChangeMode(Mode.ALL)}
|
||||
active={mode === Mode.ALL}
|
||||
onPress={() => onChangeMode(Mode.All)}
|
||||
active={mode === Mode.All}
|
||||
>
|
||||
<Text allowFontScaling={false}>Wszystkie</Text>
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => onChangeMode(Mode.SAVED)}
|
||||
active={mode === Mode.SAVED}
|
||||
onPress={() => onChangeMode(Mode.Saved)}
|
||||
active={mode === Mode.Saved}
|
||||
>
|
||||
<Text allowFontScaling={false}>Zapisane</Text>
|
||||
</Button>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import React, { forwardRef, useCallback } from 'react';
|
||||
|
||||
import {
|
||||
FlatList,
|
||||
|
@ -11,20 +11,24 @@ import ListEmpty from './ListEmpty';
|
|||
import ListLoading from './ListLoading';
|
||||
|
||||
export type Item = ListItemProps;
|
||||
export type ListProps = {
|
||||
export interface ListProps
|
||||
extends Pick<
|
||||
FlatListProps<Item>,
|
||||
'refreshing' | 'onRefresh' | 'contentContainerStyle'
|
||||
> {
|
||||
items: Item[];
|
||||
loading?: boolean;
|
||||
} & Pick<
|
||||
FlatListProps<Item>,
|
||||
'refreshing' | 'onRefresh' | 'contentContainerStyle'
|
||||
>;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
const keyExtractor = (item: ListItemProps) => item.id!;
|
||||
const renderItem = ({ item }: { item: Item }) => <ListItem {...item} />;
|
||||
|
||||
const List = forwardRef<FlatList<Item>, ListProps>(
|
||||
const MyList = forwardRef<FlatList<Item>, ListProps>(
|
||||
({ items, refreshing, onRefresh, loading, ...rest }: ListProps, ref) => {
|
||||
const renderItem = useCallback(({ item }: { item: Item }) => {
|
||||
return <ListItem {...item} />;
|
||||
}, []);
|
||||
const keyExtractor = useCallback(item => item.id, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={items}
|
||||
|
@ -57,4 +61,4 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
export default List;
|
||||
export default MyList;
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import React, { Fragment, useMemo, memo } from 'react';
|
||||
import { Icon, Left, ListItem, NativeBase, Right, Text } from 'native-base';
|
||||
|
||||
export type ListItemProps = {
|
||||
export interface ListItemProps
|
||||
extends Pick<NativeBase.ListItem, 'itemDivider' | 'itemHeader'> {
|
||||
onPress?: (id: string) => void;
|
||||
id?: string;
|
||||
text: string;
|
||||
} & Pick<NativeBase.ListItem, 'itemDivider' | 'itemHeader'>;
|
||||
}
|
||||
|
||||
const MyListItem = ({
|
||||
onPress,
|
||||
|
|
|
@ -1,31 +1,28 @@
|
|||
import { ApolloError } from '@apollo/client';
|
||||
import { useUpdateEffect } from 'react-use';
|
||||
import { Alert, Linking } from 'react-native';
|
||||
import Config from 'react-native-config';
|
||||
import buildURL from 'utils/buildURL';
|
||||
import { EMAIL } from 'config/app';
|
||||
|
||||
export type NetworkConnectionAlertProps = {
|
||||
export interface NetworkConnectionAlertProps {
|
||||
error?: ApolloError;
|
||||
};
|
||||
}
|
||||
|
||||
const NetworkConnectionAlert = ({ error }: NetworkConnectionAlertProps) => {
|
||||
useUpdateEffect(() => {
|
||||
if (!error || !error.networkError) {
|
||||
return;
|
||||
if (error && error.networkError) {
|
||||
Alert.alert(
|
||||
'Problem z połączeniem',
|
||||
'Prosimy o sprawdzenie połączenia z internetem / spróbowanie ponownie później. Przepraszamy za utrudnienia.',
|
||||
[
|
||||
{
|
||||
text: 'Zgłoś problem',
|
||||
onPress: () => Linking.openURL(buildURL('email', EMAIL)),
|
||||
},
|
||||
{ text: 'OK' },
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
'Problem z połączeniem',
|
||||
'Prosimy o sprawdzenie połączenia z internetem / spróbowanie ponownie później. Przepraszamy za utrudnienia.',
|
||||
[
|
||||
{
|
||||
text: 'Zgłoś problem',
|
||||
onPress: () =>
|
||||
Linking.openURL(buildURL('email', Config.CONTACT_EMAIL)),
|
||||
},
|
||||
{ text: 'OK' },
|
||||
],
|
||||
);
|
||||
}, [error]);
|
||||
|
||||
return null;
|
||||
|
|
|
@ -11,10 +11,10 @@ import List, { Item } from './List/List';
|
|||
import QualificationModal from './QualificationModal';
|
||||
import NetworkConnectionAlert from './NetworkConnectionAlert';
|
||||
|
||||
export type ProfessionsProps = {
|
||||
export interface ProfessionsProps {
|
||||
mode: Mode;
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
|
||||
const ID_SEPARATOR = '.';
|
||||
const getQualificationAndProfessionID = (str: string): [number, number] => {
|
||||
|
@ -44,21 +44,16 @@ const Professions = ({ mode, search }: ProfessionsProps) => {
|
|||
const [professionID, qualificationID] = getQualificationAndProfessionID(
|
||||
id,
|
||||
);
|
||||
|
||||
const profession = professions.find(p => p.id === professionID);
|
||||
if (!profession) {
|
||||
return;
|
||||
if (profession) {
|
||||
const qualification = profession.qualifications.find(
|
||||
q => q.id === qualificationID,
|
||||
);
|
||||
if (qualification) {
|
||||
setSelectedQualification(qualification);
|
||||
setIsModalVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
const qualification = profession.qualifications.find(
|
||||
q => q.id === qualificationID,
|
||||
);
|
||||
if (!qualification) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedQualification(qualification);
|
||||
setIsModalVisible(true);
|
||||
},
|
||||
[setIsModalVisible, setSelectedQualification, professions],
|
||||
);
|
||||
|
@ -67,9 +62,7 @@ const Professions = ({ mode, search }: ProfessionsProps) => {
|
|||
if (professionsLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items: Item[] = [];
|
||||
|
||||
professions.forEach(profession => {
|
||||
const qualifications = profession.qualifications
|
||||
.filter(
|
||||
|
@ -77,7 +70,7 @@ const Professions = ({ mode, search }: ProfessionsProps) => {
|
|||
(!search ||
|
||||
qualification.name.toLowerCase().includes(search) ||
|
||||
qualification.code.toLowerCase().includes(search)) &&
|
||||
(mode === Mode.ALL || isSaved(qualification.id)),
|
||||
(mode === Mode.All || isSaved(qualification.id)),
|
||||
)
|
||||
.map(
|
||||
(qualification): Item => {
|
||||
|
@ -90,21 +83,18 @@ const Professions = ({ mode, search }: ProfessionsProps) => {
|
|||
};
|
||||
},
|
||||
);
|
||||
|
||||
if (qualifications.length === 0) {
|
||||
return;
|
||||
if (qualifications.length > 0) {
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
text: profession.name,
|
||||
itemHeader: true,
|
||||
itemDivider: true,
|
||||
id: 'P' + profession.id,
|
||||
} as Item,
|
||||
...qualifications,
|
||||
];
|
||||
}
|
||||
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
text: profession.name,
|
||||
itemHeader: true,
|
||||
itemDivider: true,
|
||||
id: 'P' + profession.id,
|
||||
} as Item,
|
||||
...qualifications,
|
||||
];
|
||||
});
|
||||
|
||||
setListItems(items);
|
||||
|
|
|
@ -19,9 +19,10 @@ import {
|
|||
} from 'native-base';
|
||||
import Modal, { ModalProps } from 'common/Modal/Modal';
|
||||
|
||||
export type QualificationModalProps = {
|
||||
export interface QualificationModalProps
|
||||
extends Pick<ModalProps, 'visible' | 'onPressBackdrop'> {
|
||||
qualification: Maybe<Qualification>;
|
||||
} & Pick<ModalProps, 'visible' | 'onPressBackdrop'>;
|
||||
}
|
||||
|
||||
const QualificationModal = ({
|
||||
qualification,
|
||||
|
@ -80,7 +81,7 @@ const QualificationModal = ({
|
|||
if (onPressBackdrop) {
|
||||
onPressBackdrop();
|
||||
}
|
||||
navigation.navigate(Screen.TEST, {
|
||||
navigation.navigate(Screen.Test, {
|
||||
qualificationID: qualification?.id ?? 0,
|
||||
limit: question,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { routingInstrumentation } from '../libs/sentry/initSentry';
|
||||
import analytics from '@react-native-firebase/analytics';
|
||||
import { AppStackParamList, Screen } from 'config/routing';
|
||||
|
||||
import {
|
||||
|
@ -13,24 +13,47 @@ import TestScreen from './TestScreen/TestScreen';
|
|||
const Stack = createStackNavigator<AppStackParamList>();
|
||||
const AppStack = createStackNavigator<AppStackParamList>();
|
||||
|
||||
const AppScreens = () => {
|
||||
return (
|
||||
<AppStack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name={Screen.HOME} component={HomeScreen} />
|
||||
<Stack.Screen name={Screen.TEST} component={TestScreen} />
|
||||
</AppStack.Navigator>
|
||||
);
|
||||
};
|
||||
const AppScreens = () => (
|
||||
<AppStack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name={Screen.Home} component={HomeScreen} />
|
||||
<Stack.Screen name={Screen.Test} component={TestScreen} />
|
||||
</AppStack.Navigator>
|
||||
);
|
||||
|
||||
const Navigation = () => {
|
||||
const navigation = useRef<NavigationContainerRef>(null);
|
||||
const routeNameRef = useRef<string>('');
|
||||
const navigationRef = useRef<NavigationContainerRef>(null);
|
||||
|
||||
const logScreenView = (route: string) => {
|
||||
return analytics().logScreenView({
|
||||
screen_name: route,
|
||||
screen_class: route,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReady = () => {
|
||||
routingInstrumentation.registerNavigationContainer(navigation);
|
||||
logScreenView(navigationRef.current?.getCurrentRoute()?.name ?? '');
|
||||
};
|
||||
|
||||
const handleStateChange = () => {
|
||||
const previousRouteName = routeNameRef.current;
|
||||
const currentRouteName =
|
||||
navigationRef.current?.getCurrentRoute()?.name ?? '';
|
||||
|
||||
if (previousRouteName !== currentRouteName) {
|
||||
logScreenView(currentRouteName);
|
||||
}
|
||||
|
||||
// Save the current route name for later comparision
|
||||
routeNameRef.current = currentRouteName;
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationContainer ref={navigation} onReady={handleReady}>
|
||||
<NavigationContainer
|
||||
ref={navigationRef}
|
||||
onReady={handleReady}
|
||||
onStateChange={handleStateChange}
|
||||
>
|
||||
<AppScreens />
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
|
|
@ -15,9 +15,9 @@ import Suggestions from './components/Suggestions/Suggestions';
|
|||
import Test from './components/Test/Test';
|
||||
import Content from './components/Content/Content';
|
||||
|
||||
export type TestScreenProps = {
|
||||
route: RouteProp<AppStackParamList, Screen.TEST>;
|
||||
};
|
||||
export interface TestScreenProps {
|
||||
route: RouteProp<AppStackParamList, Screen.Test>;
|
||||
}
|
||||
|
||||
type QueryGenerateTestSimilarQualificationsQualificationArgs = {
|
||||
limitTest: Scalars['Int'];
|
||||
|
|
|
@ -2,10 +2,10 @@ import React from 'react';
|
|||
import { StyleSheet } from 'react-native';
|
||||
import { Content as ContentNB, NativeBase } from 'native-base';
|
||||
|
||||
export type ContentProps = {
|
||||
export interface ContentProps {
|
||||
children?: React.ReactNode;
|
||||
contentContainerStyle?: NativeBase.Content['contentContainerStyle'];
|
||||
};
|
||||
}
|
||||
|
||||
const Content = ({ children, contentContainerStyle }: ContentProps) => {
|
||||
return (
|
||||
|
|
|
@ -12,9 +12,9 @@ import {
|
|||
} from 'native-base';
|
||||
import Menu from 'common/Menu/Menu';
|
||||
|
||||
export type HeaderProps = {
|
||||
export interface HeaderProps {
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
|
||||
const Header = ({ title }: HeaderProps) => {
|
||||
const navigation = useNavigation();
|
||||
|
|
|
@ -9,9 +9,9 @@ import { StyleSheet } from 'react-native';
|
|||
import { H1, View, H3, Card, CardItem, Text, Button, Body } from 'native-base';
|
||||
import Content from '../Content/Content';
|
||||
|
||||
export type SuggestionsProps = {
|
||||
export interface SuggestionsProps {
|
||||
qualifications: Qualification[];
|
||||
};
|
||||
}
|
||||
|
||||
const Suggestions = ({ qualifications }: SuggestionsProps) => {
|
||||
const navigation = useNavigation();
|
||||
|
@ -51,7 +51,7 @@ const Suggestions = ({ qualifications }: SuggestionsProps) => {
|
|||
<Button
|
||||
key={question}
|
||||
onPress={() => {
|
||||
navigation.navigate(Screen.TEST, {
|
||||
navigation.navigate(Screen.Test, {
|
||||
qualificationID: qualification.id,
|
||||
limit: question,
|
||||
});
|
||||
|
|
|
@ -9,12 +9,12 @@ export enum AlertVariant {
|
|||
Warning = 'warning',
|
||||
}
|
||||
|
||||
export type AlertProps = {
|
||||
export interface AlertProps extends Pick<NativeBase.View, 'style'> {
|
||||
variant?: AlertVariant;
|
||||
title: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
} & Pick<NativeBase.View, 'style'>;
|
||||
}
|
||||
|
||||
const Alert = ({
|
||||
variant = AlertVariant.Info,
|
||||
|
|
|
@ -9,9 +9,9 @@ import {
|
|||
} from 'react-native';
|
||||
import { H3 } from 'native-base';
|
||||
|
||||
export type ImageProps = {
|
||||
export interface ImageProps extends Pick<RNImageProps, 'style'> {
|
||||
path: string;
|
||||
} & Pick<RNImageProps, 'style'>;
|
||||
}
|
||||
|
||||
const MyImage = ({ path, style }: ImageProps) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import Config from 'react-native-config';
|
||||
import { Answer, Question as QuestionT } from 'libs/graphql';
|
||||
import { useVariables } from 'libs/native-base';
|
||||
import { EMAIL } from 'config/app';
|
||||
import buildURL from 'utils/buildURL';
|
||||
|
||||
import { Linking, StyleSheet } from 'react-native';
|
||||
|
@ -10,12 +10,12 @@ import Content from '../Content/Content';
|
|||
import Image from './Image';
|
||||
import Alert, { AlertVariant } from './Alert';
|
||||
|
||||
export type QuestionProps = {
|
||||
export interface QuestionProps {
|
||||
question: QuestionT;
|
||||
reviewMode: boolean;
|
||||
selectedAnswer: Answer;
|
||||
selectAnswer: (a: Answer) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const ANSWERS = Object.values(Answer);
|
||||
|
||||
|
@ -37,9 +37,7 @@ const Question = ({
|
|||
<Button
|
||||
dark
|
||||
danger
|
||||
onPress={() =>
|
||||
Linking.openURL(buildURL('email', Config.CONTACT_EMAIL))
|
||||
}
|
||||
onPress={() => Linking.openURL(buildURL('email', EMAIL))}
|
||||
>
|
||||
<Text>Zgłoś go.</Text>
|
||||
</Button>
|
||||
|
|
|
@ -8,14 +8,14 @@ import { Button, H1, H3, Text, View } from 'native-base';
|
|||
import Content from '../Content/Content';
|
||||
import Alert from './Alert';
|
||||
|
||||
export type SummaryTabProps = {
|
||||
export interface SummaryTabProps {
|
||||
reviewMode: boolean;
|
||||
answers: Answer[];
|
||||
questions: Question[];
|
||||
finishTest: () => void;
|
||||
resetTest: () => void;
|
||||
qualificationID: number;
|
||||
};
|
||||
}
|
||||
|
||||
const SummaryTab = ({
|
||||
reviewMode,
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Answer, Qualification, Question as QuestionT } from 'libs/graphql';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import analytics from '@react-native-firebase/analytics';
|
||||
import { Question as QuestionT, Answer, Qualification } from 'libs/graphql';
|
||||
import { Event } from 'config/analytics';
|
||||
|
||||
import { ScrollableTab, Tab, Tabs } from 'native-base';
|
||||
import Question from './Question';
|
||||
import SummaryTab from './SummaryTab';
|
||||
|
||||
export type TestProps = {
|
||||
export interface TestProps {
|
||||
questions: QuestionT[];
|
||||
onReset: () => void;
|
||||
qualification: Qualification;
|
||||
};
|
||||
}
|
||||
|
||||
const Test = ({ questions, onReset, qualification }: TestProps) => {
|
||||
const [reviewMode, setReviewMode] = useState(false);
|
||||
|
@ -17,20 +19,38 @@ const Test = ({ questions, onReset, qualification }: TestProps) => {
|
|||
new Array(questions.length).fill(''),
|
||||
);
|
||||
|
||||
const analyticsParams = useMemo(
|
||||
() => ({
|
||||
qualificationID: qualification.id.toString(),
|
||||
questions: questions.length.toString(),
|
||||
}),
|
||||
[qualification, questions],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
analytics().logEvent(Event.StartTest, analyticsParams);
|
||||
}, [analyticsParams]);
|
||||
|
||||
const handleSelectAnswer = (index: number, answer: Answer) => {
|
||||
if (reviewMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedAnswers(answers =>
|
||||
answers.map((otherAnswer, index2) =>
|
||||
index === index2 ? answer : otherAnswer,
|
||||
),
|
||||
);
|
||||
analytics().logEvent(Event.SelectAnswer, {
|
||||
qualificationID: analyticsParams.qualificationID,
|
||||
questionID: questions[index].id.toString(),
|
||||
answer,
|
||||
correct: questions[index].correctAnswer === answer ? '1' : '0',
|
||||
});
|
||||
};
|
||||
|
||||
const handleFinishTest = () => {
|
||||
setReviewMode(true);
|
||||
analytics().logEvent(Event.FinishTest, analyticsParams);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import Config from 'react-native-config';
|
||||
import { CDN_URI, IMAGE_RESIZING_SERVICE } from 'config/cdn';
|
||||
|
||||
const buildURL = (type: 'cdn' | 'cdnimg' | 'email', path: string): string => {
|
||||
switch (type) {
|
||||
case 'cdn':
|
||||
return Config.CDN_URL + '/' + path;
|
||||
return CDN_URI + path;
|
||||
case 'cdnimg':
|
||||
return `${Config.WEBSITE}/_next/image?url=${
|
||||
Config.CDN_URL + '/' + encodeURIComponent(path)
|
||||
}&w=640&q=75`;
|
||||
return (
|
||||
IMAGE_RESIZING_SERVICE +
|
||||
`?url=${CDN_URI + encodeURIComponent(path)}&w=640&q=75`
|
||||
);
|
||||
case 'email':
|
||||
return `mailto:${path}`;
|
||||
}
|
||||
|
|
Reference in New Issue
Block a user