Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
7671cb863a
Bump normalize-url from 4.5.0 to 4.5.1
Bumps [normalize-url](https://github.com/sindresorhus/normalize-url) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/sindresorhus/normalize-url/releases)
- [Commits](https://github.com/sindresorhus/normalize-url/commits)

---
updated-dependencies:
- dependency-name: normalize-url
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-16 08:38:13 +00:00
48 changed files with 1318 additions and 1833 deletions

View File

@ -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
View File

@ -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

View File

@ -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.

View File

@ -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'
}

View File

@ -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">

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -0,0 +1,7 @@
{
"react-native": {
"crashlytics_debug_enabled": true,
"crashlytics_is_error_generation_on_js_crash_enabled": true,
"crashlytics_ndk_enabled": true
}
}

View File

@ -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

View File

@ -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 */

View File

@ -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

View File

@ -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
View 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
View File

@ -0,0 +1,3 @@
export const API_URI = __DEV__
? 'http://localhost:8080/graphql'
: 'https://api.zdamegzaminzawodowy.pl/graphql';

View File

@ -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
View 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`;

View File

@ -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;
};

View File

@ -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({

View File

@ -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,

View File

@ -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,
},

View File

@ -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': {

View File

@ -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,

View File

@ -211,6 +211,6 @@ export type Variables = {
};
export enum OS {
ANDROID = 'android',
Android = 'android',
IOS = 'ios',
}

View File

@ -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],
);

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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);

View File

@ -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>

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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);

View File

@ -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,
});

View File

@ -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>
);

View File

@ -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'];

View File

@ -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 (

View File

@ -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();

View File

@ -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,
});

View File

@ -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,

View File

@ -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);

View File

@ -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>

View File

@ -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,

View File

@ -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 (

View File

@ -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}`;
}

2544
yarn.lock

File diff suppressed because it is too large Load Diff