diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 2486d124..945f689b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,6 +2,8 @@
+
+
/dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-students-studentsTests/Pods-students-studentsTests-resources.sh\"\n";
showEnvVarsInLog = 0;
};
- 2AA2AD45DF43DBD6C86F260F /* [CP] Copy Pods Resources */ = {
+ 16B2240B2DE5AC5E5EF1373D /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-students-studentsTests/Pods-students-studentsTests-resources-${CONFIGURATION}-input-files.xcfilelist",
+ "${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-students-studentsTests/Pods-students-studentsTests-resources-${CONFIGURATION}-output-files.xcfilelist",
+ "${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-students-studentsTests/Pods-students-studentsTests-resources.sh\"\n";
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-resources.sh\"\n";
showEnvVarsInLog = 0;
};
- 5BC659194DA1E98530C941C5 /* [CP] Copy Pods Resources */ = {
+ 2357FBBD3292AB1C54FDC13B /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -453,7 +448,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-students-students dev/Pods-students-students dev-resources.sh\"\n";
showEnvVarsInLog = 0;
};
- 9131B957801020485B33827F /* [CP] Embed Pods Frameworks */ = {
+ 2DABAC0BA53430A70EF495EF /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -470,7 +465,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-students-students dev/Pods-students-students dev-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
- B44D6C0829F17BF2001BA0DC /* Start Packager */ = {
+ 381631C31357DD0319BEE2F7 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -478,34 +473,38 @@
inputFileListPaths = (
);
inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
);
- name = "Start Packager";
+ name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-students-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- 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";
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
- B44D6C1C29F17BF2001BA0DC /* Bundle React Native code and images */ = {
+ 4FADE446B3CDE0A64F7C222A /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
- inputPaths = (
- "$(SRCROOT)/.xcode.env.local",
- "$(SRCROOT)/.xcode.env",
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
- name = "Bundle React Native code and images";
- outputPaths = (
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "export SOURCEMAP_FILE=\"../main.jsbundle.map\";\n\nset -e\n\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
};
- B5545B0A242D45FDA2271EBA /* [CP] Check Pods Manifest.lock */ = {
+ 5B44C323B8E9DBEC9D4CD59A /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -520,31 +519,14 @@
outputFileListPaths = (
);
outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-students-studentsTests-checkManifestLockResult.txt",
+ "$(DERIVED_FILE_DIR)/Pods-students-students dev-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
- D5478D7039E563F3001F096F /* [CP] Copy Pods Resources */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-resources-${CONFIGURATION}-input-files.xcfilelist",
- );
- name = "[CP] Copy Pods Resources";
- outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-resources-${CONFIGURATION}-output-files.xcfilelist",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-resources.sh\"\n";
- showEnvVarsInLog = 0;
- };
- DCB045EA8B12F2B47D2E4386 /* [CP] Check Pods Manifest.lock */ = {
+ 67D97E4AE81B265C2F8EB10F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -559,47 +541,65 @@
outputFileListPaths = (
);
outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-students-students dev-checkManifestLockResult.txt",
+ "$(DERIVED_FILE_DIR)/Pods-students-studentsTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
- E7043618403C2A9926287F0D /* [CP] Embed Pods Frameworks */ = {
+ B37C2CF46457D5CFA96ED4B9 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ "${PODS_ROOT}/Target Support Files/Pods-students-studentsTests/Pods-students-studentsTests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ "${PODS_ROOT}/Target Support Files/Pods-students-studentsTests/Pods-students-studentsTests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-students/Pods-students-frameworks.sh\"\n";
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-students-studentsTests/Pods-students-studentsTests-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
- E70E13E9B9D96B834B9475A9 /* [CP] Embed Pods Frameworks */ = {
+ B44D6C0829F17BF2001BA0DC /* Start Packager */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-students-studentsTests/Pods-students-studentsTests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
- name = "[CP] Embed Pods Frameworks";
+ inputPaths = (
+ );
+ name = "Start Packager";
outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-students-studentsTests/Pods-students-studentsTests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-students-studentsTests/Pods-students-studentsTests-frameworks.sh\"\n";
+ 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;
};
+ B44D6C1C29F17BF2001BA0DC /* Bundle React Native code and images */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "$(SRCROOT)/.xcode.env.local",
+ "$(SRCROOT)/.xcode.env",
+ );
+ name = "Bundle React Native code and images";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "export SOURCEMAP_FILE=\"../main.jsbundle.map\";\n\nset -e\n\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
+ };
FD10A7F022414F080027D42C /* Start Packager */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -661,7 +661,7 @@
/* Begin XCBuildConfiguration section */
00E356F61AD99517003FC87E /* Debug */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 1F5CAD9CCE144ABBC4A1E2AD /* Pods-students-studentsTests.debug.xcconfig */;
+ baseConfigurationReference = 1AC5E9FB62F89FD847F90A22 /* Pods-students-studentsTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -689,7 +689,7 @@
};
00E356F71AD99517003FC87E /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = A89BF9271527FE05E0DAD5C3 /* Pods-students-studentsTests.release.xcconfig */;
+ baseConfigurationReference = AEBF1D407B9C30DA0211A468 /* Pods-students-studentsTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
COPY_PHASE_STRIP = NO;
@@ -714,12 +714,12 @@
};
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 74500F0E62F28A2E3978DD2B /* Pods-students.debug.xcconfig */;
+ baseConfigurationReference = AF5AADD6B1730D5CC1C6F305 /* Pods-students.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
- CODE_SIGN_STYLE = Manual;
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
@@ -749,14 +749,16 @@
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 8C018703C9B652EB5516599E /* Pods-students.release.xcconfig */;
+ baseConfigurationReference = A3574EB3869C9AE2FD9652E1 /* Pods-students.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = RS2Y9A4G25;
INFOPLIST_FILE = students/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "PoliTO Students";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
@@ -774,6 +776,7 @@
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "it.polito.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = students;
PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore it.polito.students";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
@@ -922,7 +925,7 @@
};
B44D6C2029F17BF2001BA0DC /* Debug */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = AA1FEA064BA1471C12F6F460 /* Pods-students-students dev.debug.xcconfig */;
+ baseConfigurationReference = E8CEE59A7533C9A24FEC8694 /* Pods-students-students dev.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev;
CLANG_ENABLE_MODULES = YES;
@@ -957,7 +960,7 @@
};
B44D6C2129F17BF2001BA0DC /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = D9C0C34BC3484398D8B75445 /* Pods-students-students dev.release.xcconfig */;
+ baseConfigurationReference = B07BE0BF3F2E37AC10DCF152 /* Pods-students-students dev.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev;
CLANG_ENABLE_MODULES = YES;
diff --git a/ios/students/Info.plist b/ios/students/Info.plist
index 61eaf7c0..d75e47b1 100644
--- a/ios/students/Info.plist
+++ b/ios/students/Info.plist
@@ -30,8 +30,10 @@
NSCameraUsageDescription
Scan documents
+ NSLocationAlwaysAndWhenInUseUsageDescription
+ Access to current position to check-in bookings
NSLocationWhenInUseUsageDescription
-
+ Check in bookings
NSPhotoLibraryUsageDescription
Scan documents and pick images for assignments and ticket attachments
UIAppFonts
diff --git a/lib/ui/components/CtaButton.tsx b/lib/ui/components/CtaButton.tsx
index c44ad92c..04b653bd 100644
--- a/lib/ui/components/CtaButton.tsx
+++ b/lib/ui/components/CtaButton.tsx
@@ -1,3 +1,4 @@
+import { useMemo } from 'react';
import {
Platform,
StyleSheet,
@@ -15,6 +16,7 @@ import { Text } from '@lib/ui/components/Text';
import { useStylesheet } from '@lib/ui/hooks/useStylesheet';
import { useTheme } from '@lib/ui/hooks/useTheme';
import { Theme } from '@lib/ui/types/Theme';
+import { shadeColor } from '@lib/ui/utils/colors';
import { useFeedbackContext } from '../../../src/core/contexts/FeedbackContext';
import { useSafeBottomBarHeight } from '../../../src/core/hooks/useSafeBottomBarHeight';
@@ -27,7 +29,9 @@ interface Props extends TouchableHighlightProps {
rightExtra?: JSX.Element;
loading?: boolean;
action: () => unknown | Promise;
+ variant?: 'filled' | 'outlined';
destructive?: boolean;
+ success?: boolean;
hint?: string;
}
@@ -41,19 +45,55 @@ export const CtaButton = ({
loading,
disabled,
destructive = false,
+ success = false,
action,
icon,
rightExtra,
hint,
containerStyle,
+ variant = 'filled',
...rest
}: Props) => {
- const { palettes, fontSizes, spacing } = useTheme();
+ const { palettes, colors, fontSizes, spacing, dark } = useTheme();
const styles = useStylesheet(createStyles);
const { left, right } = useSafeAreaInsets();
const bottomBarHeight = useSafeBottomBarHeight();
const { isFeedbackVisible } = useFeedbackContext();
+ const outlined = variant === 'outlined';
+
+ const underlayColor = useMemo(() => {
+ if (variant === 'outlined') {
+ if (dark) return shadeColor(colors.background, 20);
+ else return shadeColor(colors.background, -10);
+ } else {
+ if (destructive) return palettes.danger[700];
+ return palettes.primary[500];
+ }
+ }, [
+ colors.background,
+ dark,
+ destructive,
+ palettes.danger,
+ palettes.primary,
+ variant,
+ ]);
+
+ const color = useMemo(() => {
+ if (success) {
+ return dark ? palettes.success[400] : palettes.success[700];
+ }
+ if (destructive) return palettes.danger[600];
+ return palettes.primary[400];
+ }, [
+ dark,
+ destructive,
+ palettes.danger,
+ palettes.primary,
+ palettes.success,
+ success,
+ ]);
+
return (
{hint}}
- {loading && }
+ {loading && (
+
+ )}
{/* {!loading && ( */}
@@ -102,11 +153,23 @@ export const CtaButton = ({
)}
- {title}
+
+ {title}
+
{rightExtra && rightExtra}
@@ -158,7 +221,7 @@ const createStyles = ({
fontSize: fontSizes.md,
fontWeight: fontWeights.medium,
textAlign: 'center',
- color: 'white',
+ color: colors.white,
},
icon: {
marginVertical: -2,
diff --git a/lib/ui/components/CtaButtonContainer.tsx b/lib/ui/components/CtaButtonContainer.tsx
new file mode 100644
index 00000000..e1e32482
--- /dev/null
+++ b/lib/ui/components/CtaButtonContainer.tsx
@@ -0,0 +1,45 @@
+import { Children, PropsWithChildren } from 'react';
+import { Platform, View } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { useTheme } from '@lib/ui/hooks/useTheme';
+
+import { useFeedbackContext } from '../../../src/core/contexts/FeedbackContext';
+import { useSafeBottomBarHeight } from '../../../src/core/hooks/useSafeBottomBarHeight';
+
+interface Props {
+ absolute: boolean;
+}
+
+export const CtaButtonContainer = ({
+ absolute = true,
+ children,
+}: PropsWithChildren) => {
+ const { left, right } = useSafeAreaInsets();
+ const bottomBarHeight = useSafeBottomBarHeight();
+ const { isFeedbackVisible } = useFeedbackContext();
+
+ const { spacing } = useTheme();
+ return (
+
+ {children}
+
+ );
+};
diff --git a/lib/ui/components/ScreenDateTime.tsx b/lib/ui/components/ScreenDateTime.tsx
new file mode 100644
index 00000000..b4dc49ef
--- /dev/null
+++ b/lib/ui/components/ScreenDateTime.tsx
@@ -0,0 +1,41 @@
+import { useMemo } from 'react';
+
+import { faCalendar, faClock } from '@fortawesome/free-regular-svg-icons';
+import { Icon } from '@lib/ui/components/Icon';
+import { Row } from '@lib/ui/components/Row';
+import { Text } from '@lib/ui/components/Text';
+import { useTheme } from '@lib/ui/hooks/useTheme';
+
+interface Props {
+ accessible?: boolean;
+ date?: string;
+ time?: string;
+ inListItem?: boolean;
+}
+
+export const ScreenDateTime = ({
+ accessible,
+ date,
+ time,
+ inListItem = false,
+}: Props) => {
+ const { colors, dark, fontSizes, palettes } = useTheme();
+
+ const color = useMemo(() => {
+ if (!inListItem) return colors.prose;
+ return dark ? palettes.gray[400] : palettes.gray[500];
+ }, [colors.prose, dark, inListItem, palettes.gray]);
+
+ return (
+
+
+
+ {date ?? ''}
+
+
+
+ {time ?? ''}
+
+
+ );
+};
diff --git a/lib/ui/components/Section.tsx b/lib/ui/components/Section.tsx
index 0073c6f7..4ce58583 100644
--- a/lib/ui/components/Section.tsx
+++ b/lib/ui/components/Section.tsx
@@ -1,13 +1,13 @@
import { PropsWithChildren } from 'react';
import { Platform, ViewProps } from 'react-native';
-import { Col } from '@lib/ui/components/Col';
+import { Col, ColProps } from '@lib/ui/components/Col';
export const Section = ({
style,
children,
...rest
-}: PropsWithChildren) => {
+}: PropsWithChildren) => {
return (
=16.0.0"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@react-native-community/netinfo": {
"version": "9.3.4",
"license": "MIT",
@@ -6139,6 +6154,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/geopoint": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@types/geopoint/-/geopoint-1.0.1.tgz",
+ "integrity": "sha512-u0NMCW2hIxV970oUbyzfwi3aRiAghvUfbEtq3yDHoKgda67bUMsXl3Q7J6kKCFWbjZuQo6n+79IIT5mRTy6UlA==",
+ "dev": true
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.6",
"dev": true,
@@ -10175,6 +10196,14 @@
"node": ">=6.9.0"
}
},
+ "node_modules/geopoint": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/geopoint/-/geopoint-1.0.1.tgz",
+ "integrity": "sha512-ojNWKDu4hdY2MeLUIiJH9cjP2s2BrVf9P2tuIvUPS8Hht0FzZpoZEAEmOa21eUCi8ahk1XBD3reSjPjsyVujkg==",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"license": "ISC",
diff --git a/package.json b/package.json
index fece4ab4..af4b7346 100644
--- a/package.json
+++ b/package.json
@@ -24,9 +24,10 @@
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/react-native-fontawesome": "^0.3.0",
"@miblanchard/react-native-slider": "^2.2.0",
- "@polito/api-client": "1.0.0-ALPHA.40",
+ "@polito/api-client": "^1.0.0-ALPHA.41",
"@react-native-async-storage/async-storage": "^1.17.11",
"@react-native-community/blur": "^4.3.0",
+ "@react-native-community/geolocation": "^3.1.0",
"@react-native-community/netinfo": "^9.3.4",
"@react-native-menu/menu": "^0.7.3",
"@react-navigation/bottom-tabs": "^6.5.8",
@@ -43,6 +44,7 @@
"@typescript-eslint/eslint-plugin": "^5.50.0",
"color": "^4.2.3",
"domutils": "^3.0.1",
+ "geopoint": "^1.0.1",
"htmlparser2": "^8.0.1",
"i18next": "^21.10.0",
"intl": "^1.2.5",
@@ -92,6 +94,7 @@
"@trivago/prettier-plugin-sort-imports": "^3.3.0",
"@tsconfig/react-native": "^2.0.2",
"@types/color": "^3.0.3",
+ "@types/geopoint": "^1.0.1",
"@types/jest": "^29.2.1",
"@types/lodash": "^4.14.192",
"@types/luxon": "^3.2.0",
diff --git a/src/core/components/EventDetails.tsx b/src/core/components/EventDetails.tsx
index a871ec01..e6633a72 100644
--- a/src/core/components/EventDetails.tsx
+++ b/src/core/components/EventDetails.tsx
@@ -9,7 +9,7 @@ import { formatTime } from '../../utils/dates';
type Props = ViewProps & {
title?: string;
type?: string;
- time?: string;
+ time?: string | JSX.Element;
endTime?: Date;
timeLabel?: string;
};
@@ -29,12 +29,15 @@ export const EventDetails = ({
{type}
- {time && (
-
- {time}
- {endTime && ` - ${formatTime(endTime)}`}
-
- )}
+ {time &&
+ (typeof time === 'string' ? (
+
+ {time}
+ {endTime && ` - ${formatTime(endTime)}`}
+
+ ) : (
+ time
+ ))}
{!!timeLabel && (
{timeLabel}
)}
diff --git a/src/core/hooks/useGeolocation.ts b/src/core/hooks/useGeolocation.ts
new file mode 100644
index 00000000..441ec384
--- /dev/null
+++ b/src/core/hooks/useGeolocation.ts
@@ -0,0 +1,88 @@
+import { useCallback, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Linking } from 'react-native';
+
+import Geolocation from '@react-native-community/geolocation';
+
+import GeoPoint from 'geopoint';
+
+import { IS_ANDROID } from '../constants';
+import { useFeedbackContext } from '../contexts/FeedbackContext';
+
+export type Coordinates = {
+ latitude: number;
+ longitude: number;
+};
+
+Geolocation.setRNConfiguration({
+ skipPermissionRequests: false,
+ authorizationLevel: 'whenInUse',
+ enableBackgroundLocationUpdates: true,
+ locationProvider: 'auto',
+});
+
+const openSettings = () => {
+ if (IS_ANDROID) {
+ Linking.sendIntent('android.settings.LOCATION_SOURCE_SETTINGS').catch(
+ console.error,
+ );
+ } else {
+ Linking.openURL('app-settings:').catch(console.error);
+ }
+};
+
+export const computeDistance = (a?: Coordinates, b?: Coordinates) => {
+ if (!a || !b) return 0;
+
+ const pointA = new GeoPoint(a.latitude, a.longitude);
+ const pointB = new GeoPoint(b.latitude, b.longitude);
+
+ return pointA.distanceTo(pointB, true);
+};
+
+export const useGeolocation = () => {
+ const { setFeedback } = useFeedbackContext();
+ const { t } = useTranslation();
+
+ const getCurrentPosition = useCallback(() => {
+ return new Promise((resolve, reject) => {
+ Geolocation.getCurrentPosition(
+ ({ coords: { latitude, longitude } }) => {
+ resolve({ latitude, longitude });
+ },
+ error => {
+ if (error.code === 2) {
+ setFeedback({
+ text: t('common.enableLocationServiceFeedback'),
+ isPersistent: false,
+ });
+ }
+ reject(error);
+ },
+ );
+ });
+ }, [t]);
+
+ useEffect(() => {
+ Geolocation.requestAuthorization(
+ () => console.debug('Geolocation permission granted'),
+ error => {
+ if (error.code === 1) {
+ setFeedback({
+ text: t('common.enableLocationPermissionFeedback'),
+ action: {
+ label: t('common.openSettings'),
+ onPress: openSettings,
+ },
+ isPersistent: true,
+ });
+ }
+ },
+ );
+ }, [t]);
+
+ return {
+ getCurrentPosition,
+ computeDistance,
+ };
+};
diff --git a/src/core/queries/bookingHooks.ts b/src/core/queries/bookingHooks.ts
index fbdc2773..818a3a47 100644
--- a/src/core/queries/bookingHooks.ts
+++ b/src/core/queries/bookingHooks.ts
@@ -1,9 +1,14 @@
import { BookingsApi } from '@polito/api-client';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { DateTime } from 'luxon';
+
import { pluckData } from '../../utils/queries';
export const BOOKINGS_QUERY_KEY = ['bookings'];
+export const BOOKINGS_TOPICS_QUERY_KEY = ['bookings', 'topics'];
+export const BOOKINGS_SLOTS_QUERY_KEY = ['bookings', 'slots'];
+export const BOOKINGS_SEATS_QUERY_KEY = ['bookings', 'seats'];
const useBookingClient = (): BookingsApi => {
return new BookingsApi();
@@ -17,6 +22,92 @@ export const useGetBookings = () => {
);
};
+export const useGetBookingTopics = () => {
+ const bookingClient = useBookingClient();
+
+ return useQuery(BOOKINGS_TOPICS_QUERY_KEY, () =>
+ bookingClient.getBookingTopics().then(pluckData),
+ );
+};
+
+export const useGetBookingSlots = (bookingTopicId: string) => {
+ const bookingClient = useBookingClient();
+
+ return useQuery(BOOKINGS_SLOTS_QUERY_KEY, () =>
+ bookingClient
+ .getBookingSlots({
+ bookingTopicId,
+ fromDate: DateTime.fromObject({
+ day: 1,
+ month: 11,
+ year: 2023,
+ }).toJSDate(),
+ toDate: DateTime.fromObject({
+ day: 3,
+ month: 11,
+ year: 2023,
+ }).toJSDate(),
+ })
+ .then(pluckData),
+ );
+};
+
+export const useGetBookingSeats = (
+ bookingTopicId: string,
+ bookingSlotId: string,
+) => {
+ const bookingClient = useBookingClient();
+
+ return useQuery(BOOKINGS_SEATS_QUERY_KEY, () =>
+ bookingClient
+ .getBookingSeats({ bookingTopicId, bookingSlotId })
+ .then(pluckData),
+ );
+};
+
+export const useUpdateBooking = () => {
+ const bookingClient = useBookingClient();
+ const queryClient = useQueryClient();
+ return useMutation(
+ ({
+ bookingId,
+ isLocationChecked,
+ }: {
+ bookingId: number;
+ isLocationChecked: boolean;
+ }) =>
+ bookingClient.updateBooking({
+ bookingId,
+ updateBookingRequest: { isLocationChecked },
+ }),
+ {
+ onSuccess() {
+ return queryClient.invalidateQueries(BOOKINGS_QUERY_KEY);
+ },
+ },
+ );
+};
+
+export const useCreateBooking = () => {
+ const bookingClient = useBookingClient();
+ const client = useQueryClient();
+
+ return useMutation(
+ ({ slotId, seatId }: { slotId: number; seatId?: number }) =>
+ bookingClient.createBooking({
+ createBookingRequest: {
+ slotId,
+ seatId,
+ },
+ }),
+ {
+ onSuccess() {
+ return client.invalidateQueries(BOOKINGS_QUERY_KEY);
+ },
+ },
+ );
+};
+
export const useDeleteBooking = (bookingId: number) => {
const bookingClient = useBookingClient();
const client = useQueryClient();
diff --git a/src/core/queries/courseHooks.ts b/src/core/queries/courseHooks.ts
index 4a2c1b99..4a1083f4 100644
--- a/src/core/queries/courseHooks.ts
+++ b/src/core/queries/courseHooks.ts
@@ -6,11 +6,11 @@ import {
CourseDirectoryContentInner,
CourseFileOverview,
CourseOverview,
+ CourseOverviewPreviousEditionsInner,
+ CourseVcOtherCoursesInner,
CoursesApi,
UploadCourseAssignmentRequest,
} from '@polito/api-client';
-import { CourseOverviewPreviousEditionsInner } from '@polito/api-client/models/CourseOverviewPreviousEditionsInner';
-import { CourseVcOtherCoursesInner } from '@polito/api-client/models/CourseVcOtherCoursesInner';
import {
useMutation,
useQueries,
diff --git a/src/core/themes/light.ts b/src/core/themes/light.ts
index c0dafce2..de9ec0f9 100644
--- a/src/core/themes/light.ts
+++ b/src/core/themes/light.ts
@@ -118,6 +118,7 @@ export const lightTheme: Theme = {
background: backgroundColor,
surface: '#FFFFFF',
surfaceDark: '#143959',
+ white: '#FFFFFF',
headersBackground: IS_ANDROID ? '#FFFFFF' : '#EDEEF0',
heading: navy[700],
subHeading: lightBlue[700],
diff --git a/src/features/agenda/components/ExamCard.tsx b/src/features/agenda/components/ExamCard.tsx
index 7dfb56ff..f3ae7f57 100644
--- a/src/features/agenda/components/ExamCard.tsx
+++ b/src/features/agenda/components/ExamCard.tsx
@@ -28,7 +28,7 @@ export const ExamCard = ({ item, compact = false }: Props) => {
time={
item.isTimeToBeDefined ? t('common.timeToBeDefined') : item.fromTime
}
- location={item.classroom}
+ location={item.places?.map(place => place.name).join(', ')}
onPress={() =>
navigate({
name: 'Exam',
diff --git a/src/features/agenda/queries/agendaHooks.ts b/src/features/agenda/queries/agendaHooks.ts
index 29131e67..e0c5c868 100644
--- a/src/features/agenda/queries/agendaHooks.ts
+++ b/src/features/agenda/queries/agendaHooks.ts
@@ -55,7 +55,7 @@ const groupItemsByDay = (
fromTime: formatTime(exam.examStartsAt!),
isTimeToBeDefined: exam.isTimeToBeDefined,
title: exam.courseName,
- classroom: exam?.classrooms,
+ places: exam?.places ?? [],
teacherId: exam.teacherId,
};
return item;
@@ -112,7 +112,7 @@ const groupItemsByDay = (
startDate.setHours(0, 0, 0);
const item: DeadlineItem = {
- key: 'deadline' + deadline.date.valueOf(),
+ key: 'deadline-' + deadline.id,
start: DateTime.fromJSDate(startDate),
end: DateTime.fromJSDate(startDate).plus({ hour: 1 }),
startTimestamp: deadline.date.valueOf(),
diff --git a/src/features/agenda/screens/BookingScreen.tsx b/src/features/agenda/screens/BookingScreen.tsx
index f41885a1..516a6256 100644
--- a/src/features/agenda/screens/BookingScreen.tsx
+++ b/src/features/agenda/screens/BookingScreen.tsx
@@ -1,76 +1,153 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { Platform, SafeAreaView, ScrollView, StyleSheet } from 'react-native';
+import {
+ Linking,
+ Platform,
+ SafeAreaView,
+ ScrollView,
+ StyleSheet,
+ View,
+} from 'react-native';
import Barcode from 'react-native-barcode-svg';
-import { faLocation } from '@fortawesome/free-solid-svg-icons';
+import { faCheckCircle, faLocation } from '@fortawesome/free-solid-svg-icons';
import { Card } from '@lib/ui/components/Card';
import { CtaButton, CtaButtonSpacer } from '@lib/ui/components/CtaButton';
+import { CtaButtonContainer } from '@lib/ui/components/CtaButtonContainer';
import { Icon } from '@lib/ui/components/Icon';
import { ListItem } from '@lib/ui/components/ListItem';
import { OverviewList } from '@lib/ui/components/OverviewList';
import { RefreshControl } from '@lib/ui/components/RefreshControl';
+import { ScreenTitle } from '@lib/ui/components/ScreenTitle';
import { Section } from '@lib/ui/components/Section';
-import { Separator } from '@lib/ui/components/Separator';
+import { SectionHeader } from '@lib/ui/components/SectionHeader';
import { Text } from '@lib/ui/components/Text';
import { useStylesheet } from '@lib/ui/hooks/useStylesheet';
import { useTheme } from '@lib/ui/hooks/useTheme';
import { Theme } from '@lib/ui/types/Theme';
+import { isToday } from '@lib/ui/utils/calendar';
import { Booking } from '@polito/api-client';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { DateTime } from 'luxon';
+
import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer';
-import { EventDetails } from '../../../core/components/EventDetails';
import { useFeedbackContext } from '../../../core/contexts/FeedbackContext';
+import { useConfirmationDialog } from '../../../core/hooks/useConfirmationDialog';
+import { useGeolocation } from '../../../core/hooks/useGeolocation';
import { useOfflineDisabled } from '../../../core/hooks/useOfflineDisabled';
import {
useDeleteBooking,
useGetBookings,
+ useUpdateBooking,
} from '../../../core/queries/bookingHooks';
import { useGetStudent } from '../../../core/queries/studentHooks';
-import { formatDateTime, formatTime } from '../../../utils/dates';
+import { BookingDateTime } from '../../bookings/components/BookingDateTime';
import { AgendaStackParamList } from '../components/AgendaNavigator';
type Props = NativeStackScreenProps;
+const bookingLocationHasValidCoordinates = (
+ location: Booking['locationCheck'],
+) => {
+ return !!location?.latitude && !!location?.longitude && !!location.radiusInKm;
+};
+
export const BookingScreen = ({ navigation, route }: Props) => {
const { id } = route.params;
const { t } = useTranslation();
const { setFeedback } = useFeedbackContext();
-
+ const { getCurrentPosition, computeDistance } = useGeolocation();
const { colors, palettes, spacing } = useTheme();
const bookingsQuery = useGetBookings();
- const bookingMutation = useDeleteBooking(id);
+ const deleteBookingMutation = useDeleteBooking(id);
+ const updateBookingMutation = useUpdateBooking();
const studentQuery = useGetStudent();
+ const confirmCancel = useConfirmationDialog({
+ title: t('bookingScreen.cancelBooking'),
+ message: t('bookingScreen.cancelBookingText'),
+ });
const isDisabled = useOfflineDisabled();
const styles = useStylesheet(createStyles);
const booking = bookingsQuery.data?.find((e: Booking) => e.id === id);
const title = booking?.topic?.title ?? '';
- const timeLabel = useMemo(() => {
- if (!booking) return '';
- const fromDate = formatDateTime(booking.startsAt);
- const toTime = formatTime(booking.endsAt);
- return `${fromDate} - ${toTime}`;
- }, [booking]);
+ const subTopicTitle = booking?.subtopic?.title ?? '';
- const onPressLocation = () => {};
+ const hasCheckIn = useMemo(
+ () =>
+ booking?.startsAt &&
+ isToday(DateTime.fromJSDate(booking?.startsAt)) &&
+ booking?.locationCheck?.enabled &&
+ bookingLocationHasValidCoordinates(booking?.locationCheck),
+ [booking],
+ );
+
+ const completedCheckIn = useMemo(
+ () => hasCheckIn && booking?.locationCheck?.checked,
+ [booking?.locationCheck?.checked, hasCheckIn],
+ );
- const onPressDelete = () => {
- bookingMutation
- .mutateAsync()
- .then(() => navigation.goBack())
- .then(() => setFeedback({ text: t('bookingScreen.cancelFeedback') }));
+ const canBeCancelled = useMemo(
+ () =>
+ !!booking?.cancelableUntil &&
+ booking?.cancelableUntil.getTime() > Date.now(),
+ [booking],
+ );
+
+ const onPressCheckIn = async () => {
+ if (!booking?.id) return;
+ getCurrentPosition().then(currentDeviceCoordinates => {
+ const computedDistance = computeDistance(currentDeviceCoordinates, {
+ latitude: Number(booking?.locationCheck?.latitude),
+ longitude: Number(booking?.locationCheck?.longitude),
+ });
+ if (computedDistance < Number(booking?.locationCheck?.radiusInKm)) {
+ updateBookingMutation
+ .mutateAsync({
+ bookingId: booking.id,
+ isLocationChecked: true,
+ })
+ .then(() => {
+ setFeedback({ text: t('bookingScreen.checkInFeedback') });
+ });
+ } else {
+ setFeedback({ text: t('bookingScreen.checkLocationErrorFeedback') });
+ }
+ });
+ };
+
+ const onPressLocation = async (location: Booking['location']) => {
+ if (location.type === 'virtualPlace') {
+ await Linking.openURL(location.url);
+ }
+ };
+
+ const onPressDelete = async () => {
+ if (await confirmCancel()) {
+ setFeedback({ text: t('bookingScreen.cancelFeedback') });
+ return deleteBookingMutation
+ .mutateAsync()
+ .then(() => navigation.goBack())
+ .then(() => setFeedback({ text: t('bookingScreen.cancelFeedback') }));
+ }
};
return (
<>
}
+ refreshControl={}
>
-
+
+
+ {subTopicTitle && (
+
+ {subTopicTitle}
+
+ )}
+
+
{booking?.location?.name && (
{
/>
}
title={booking.location.name}
- subtitle={booking.location?.type}
- onPress={onPressLocation}
+ subtitle={t(
+ `bookingScreen.locationType.${booking.location?.type}`,
+ )}
+ onPress={() => onPressLocation(booking?.location)}
/>
)}
-
-
-
+
+
+
{studentQuery.data && (
{
)}
-
+ {hasCheckIn && }
+ {canBeCancelled && }
- {booking?.canBeCancelled && (
-
- )}
+
+ {hasCheckIn && (
+
+ )}
+ {canBeCancelled && (
+
+ )}
+
>
);
};
-const createStyles = ({ spacing, palettes, fontSizes }: Theme) =>
+const createStyles = ({ spacing, fontSizes }: Theme) =>
StyleSheet.create({
barCodeCard: {
- width: '100%',
padding: fontSizes.md,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
- borderRadius: fontSizes.md,
marginHorizontal: Platform.select({ ios: spacing[4] }),
},
- sectionSeparator: {
- paddingHorizontal: fontSizes.lg,
- marginTop: fontSizes.xs,
- },
- sectionContainer: {
- paddingHorizontal: fontSizes.md,
- flexDirection: 'column',
- justifyContent: 'center',
- alignItems: 'center',
- },
- wrapper: {
- marginTop: fontSizes.xs,
- // padding: fontSizes.sm,
- },
- booking: {
- color: palettes.primary[400],
- textTransform: 'uppercase',
- marginVertical: fontSizes.sm,
- },
- time: {
- textTransform: 'capitalize',
- },
});
diff --git a/src/features/agenda/screens/LectureScreen.tsx b/src/features/agenda/screens/LectureScreen.tsx
index e10fc652..94a2e076 100644
--- a/src/features/agenda/screens/LectureScreen.tsx
+++ b/src/features/agenda/screens/LectureScreen.tsx
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { SafeAreaView, ScrollView } from 'react-native';
+import { SafeAreaView, ScrollView, View } from 'react-native';
import { faLocationDot } from '@fortawesome/free-solid-svg-icons';
import { Icon } from '@lib/ui/components/Icon';
@@ -19,6 +19,11 @@ import { useGetPerson } from '../../../core/queries/peopleHooks';
import { GlobalStyles } from '../../../core/styles/globalStyles';
import { convertMachineDateToFormatDate } from '../../../utils/dates';
import { CourseIcon } from '../../teaching/components/CourseIcon';
+import {
+ isLiveVC,
+ isRecordedVC,
+ isVideoLecture,
+} from '../../teaching/utils/lectures';
import { AgendaStackParamList } from '../components/AgendaNavigator';
type Props = NativeStackScreenProps;
@@ -47,11 +52,18 @@ export const LectureScreen = ({ route }: Props) => {
contentContainerStyle={GlobalStyles.fillHeight}
>
- {virtualClassroom?.videoUrl && (
-
+ {lecture &&
+ (isRecordedVC(lecture) || isVideoLecture(lecture)) &&
+ lecture.videoUrl && (
+
+ )}
+ {lecture && isLiveVC(lecture) && (
+
+ // TODO handle live VC
+ //
)}
{
+ return (
+
+ );
+};
diff --git a/src/features/offering/screens/StaffScreen.tsx b/src/features/offering/screens/StaffScreen.tsx
index 84db12e6..245a315c 100644
--- a/src/features/offering/screens/StaffScreen.tsx
+++ b/src/features/offering/screens/StaffScreen.tsx
@@ -3,7 +3,7 @@ import { SafeAreaView, ScrollView } from 'react-native';
import { OverviewList } from '@lib/ui/components/OverviewList';
import { Section } from '@lib/ui/components/Section';
-import { OfferingCourseStaffInner } from '@polito/api-client/models';
+import { OfferingCourseStaff } from '@polito/api-client/models';
import { Person } from '@polito/api-client/models/Person';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
@@ -20,12 +20,12 @@ export const StaffScreen = ({ route }: Props) => {
const { queries: staffQueries, isLoading } = useGetPersons(staffIds);
- const staffPeople: (Person & OfferingCourseStaffInner)[] = useMemo(() => {
+ const staffPeople: (Person & OfferingCourseStaff)[] = useMemo(() => {
if (isLoading) {
return [];
}
- const staffData: (Person & OfferingCourseStaffInner)[] = [];
+ const staffData: (Person & OfferingCourseStaff)[] = [];
staffQueries.forEach((staffQuery, index) => {
if (!staffQuery.data) return;
diff --git a/src/features/services/components/BookingListItem.tsx b/src/features/services/components/BookingListItem.tsx
new file mode 100644
index 00000000..95c7fd18
--- /dev/null
+++ b/src/features/services/components/BookingListItem.tsx
@@ -0,0 +1,80 @@
+import { StyleSheet } from 'react-native';
+
+import { faChevronRight } from '@fortawesome/free-solid-svg-icons';
+import { Icon } from '@lib/ui/components/Icon';
+import { ListItem } from '@lib/ui/components/ListItem';
+import { useStylesheet } from '@lib/ui/hooks/useStylesheet';
+import { useTheme } from '@lib/ui/hooks/useTheme';
+import { Theme } from '@lib/ui/types/Theme';
+import { Booking } from '@polito/api-client';
+
+import { DateTime } from 'luxon';
+
+import { useAccessibility } from '../../../core/hooks/useAccessibilty';
+import { getHtmlTextContent } from '../../../utils/html';
+import { BookingDateTime } from '../../bookings/components/BookingDateTime';
+
+interface Props {
+ booking: Booking;
+ index: number;
+ totalData: number;
+}
+
+export const BookingListItem = ({ booking, index, totalData }: Props) => {
+ const { colors } = useTheme();
+ const styles = useStylesheet(createStyles);
+ const { accessibilityListLabel } = useAccessibility();
+ const date = DateTime.fromJSDate(booking?.startsAt).toFormat('dd MMMM');
+ const startsAtTime = DateTime.fromJSDate(booking?.startsAt).toFormat('HH:mm');
+ const endAtTime = DateTime.fromJSDate(booking?.endsAt).toFormat('HH:mm');
+
+ const accessibilityLabel = accessibilityListLabel(index, totalData);
+ const title = getHtmlTextContent(booking?.topic?.title ?? '');
+
+ return (
+ }
+ accessibilityLabel={[
+ accessibilityLabel,
+ title,
+ date,
+ `${startsAtTime} - ${endAtTime}`,
+ ].join(', ')}
+ subtitleStyle={styles.subtitle}
+ trailingItem={
+
+ }
+ />
+ );
+};
+
+const createStyles = ({ spacing, fontSizes, fontWeights, palettes }: Theme) =>
+ StyleSheet.create({
+ title: {
+ fontSize: fontSizes.md,
+ fontWeight: fontWeights.medium,
+ },
+ subtitle: {
+ color: palettes.text['500'],
+ fontWeight: fontWeights.medium,
+ textTransform: 'capitalize',
+ fontSize: fontSizes.sm,
+ marginTop: spacing[0.5],
+ },
+ icon: {
+ marginRight: -spacing[1],
+ },
+ });
diff --git a/src/features/services/components/ServicesNavigator.tsx b/src/features/services/components/ServicesNavigator.tsx
index ebe61a46..25044a6a 100644
--- a/src/features/services/components/ServicesNavigator.tsx
+++ b/src/features/services/components/ServicesNavigator.tsx
@@ -3,13 +3,14 @@ import { Platform } from 'react-native';
import { useTheme } from '@lib/ui/hooks/useTheme';
import { TicketStatus } from '@polito/api-client';
-import { OfferingCourseStaffInner } from '@polito/api-client/models';
+import { OfferingCourseStaff } from '@polito/api-client/models';
import { TicketFAQ } from '@polito/api-client/models/TicketFAQ';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { HeaderCloseButton } from '../../../core/components/HeaderCloseButton';
import { HeaderLogo } from '../../../core/components/HeaderLogo';
import { useTitlesStyles } from '../../../core/hooks/useTitlesStyles';
+import { BookingScreen } from '../../agenda/screens/BookingScreen';
import { DegreeTopTabsNavigator } from '../../offering/navigation/DegreeTopTabsNavigator';
import { OfferingTopTabsNavigator } from '../../offering/navigation/OfferingTopTabsNavigator';
import { DegreeCourseGuideScreen } from '../../offering/screens/DegreeCourseGuideScreen';
@@ -17,10 +18,12 @@ import { DegreeCourseScreen } from '../../offering/screens/DegreeCourseScreen';
import { StaffScreen } from '../../offering/screens/StaffScreen';
import { PersonScreen } from '../../teaching/screens/PersonScreen';
import { UnreadMessagesModal } from '../../user/screens/UnreadMessagesModal';
+import { BookingsScreen } from '../screens/BookingsScreen';
import { ContactsScreen } from '../screens/ContactsScreen';
import { CreateTicketScreen } from '../screens/CreateTicketScreen';
import { JobOfferScreen } from '../screens/JobOfferScreen';
import { JobOffersScreen } from '../screens/JobOffersScreen';
+import { NewBookingScreen } from '../screens/NewBookingScreen';
import { NewsItemScreen } from '../screens/NewsItemScreen';
import { NewsScreen } from '../screens/NewsScreen';
import { ServicesScreen } from '../screens/ServicesScreen';
@@ -41,7 +44,7 @@ export type OfferingStackParamList = {
courseShortcode: string;
year?: string;
};
- Staff: { staff: OfferingCourseStaffInner[] };
+ Staff: { staff: OfferingCourseStaff[] };
};
export type ServiceStackParamList = OfferingStackParamList & {
@@ -66,6 +69,9 @@ export type ServiceStackParamList = OfferingStackParamList & {
MessagesModal: undefined;
Contacts: undefined;
Person: { id: number };
+ Bookings: undefined;
+ Booking: { id: number };
+ NewBooking: undefined;
};
const Stack = createNativeStackNavigator();
@@ -254,6 +260,31 @@ export const ServicesNavigator = () => {
headerBackTitleVisible: false,
}}
/>
+
+
+
);
diff --git a/src/features/services/screens/BookingsScreen.tsx b/src/features/services/screens/BookingsScreen.tsx
new file mode 100644
index 00000000..7ec7acbc
--- /dev/null
+++ b/src/features/services/screens/BookingsScreen.tsx
@@ -0,0 +1,64 @@
+import { useTranslation } from 'react-i18next';
+import { SafeAreaView, ScrollView } from 'react-native';
+
+import { faPlus } from '@fortawesome/free-solid-svg-icons';
+import { CtaButton } from '@lib/ui/components/CtaButton';
+import { OverviewList } from '@lib/ui/components/OverviewList';
+import { RefreshControl } from '@lib/ui/components/RefreshControl';
+import { Section } from '@lib/ui/components/Section';
+import { SectionHeader } from '@lib/ui/components/SectionHeader';
+import { useTheme } from '@lib/ui/hooks/useTheme';
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+
+import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer';
+import { useGetBookings } from '../../../core/queries/bookingHooks';
+import { BookingListItem } from '../components/BookingListItem';
+import { ServiceStackParamList } from '../components/ServicesNavigator';
+
+type Props = NativeStackScreenProps;
+const createBookingEnabled = false;
+
+export const BookingsScreen = ({ navigation }: Props) => {
+ const bookingsQuery = useGetBookings();
+ const { t } = useTranslation();
+ const { spacing } = useTheme();
+
+ return (
+ <>
+ }
+ >
+
+
+
+
+ {bookingsQuery?.data?.map((booking, index) => (
+
+ ))}
+
+
+
+
+
+ {createBookingEnabled && (
+ {
+ navigation.goBack();
+ navigation.navigate('NewBooking');
+ }}
+ title={t('bookingsScreen.newBooking')}
+ icon={faPlus}
+ />
+ )}
+ >
+ );
+};
diff --git a/src/features/services/screens/NewBookingScreen.tsx b/src/features/services/screens/NewBookingScreen.tsx
new file mode 100644
index 00000000..8c6b88d8
--- /dev/null
+++ b/src/features/services/screens/NewBookingScreen.tsx
@@ -0,0 +1,22 @@
+import { ScrollView } from 'react-native';
+
+import { Text } from '@lib/ui/components/Text';
+
+import {
+ useGetBookingSlots,
+ useGetBookingTopics,
+} from '../../../core/queries/bookingHooks';
+
+export const NewBookingScreen = () => {
+ const topicsQuery = useGetBookingTopics();
+ const bookingsSlotsQuery = useGetBookingSlots('TEST_COORDINATE');
+
+ console.debug('topicsQuery', topicsQuery?.data);
+ console.debug('bookingsSlotsQuery', bookingsSlotsQuery?.data);
+
+ return (
+
+
+
+ );
+};
diff --git a/src/features/services/screens/ServicesScreen.tsx b/src/features/services/screens/ServicesScreen.tsx
index 2bef96bd..044d92b9 100644
--- a/src/features/services/screens/ServicesScreen.tsx
+++ b/src/features/services/screens/ServicesScreen.tsx
@@ -22,6 +22,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer';
import { usePreferencesContext } from '../../../core/contexts/PreferencesContext';
import { useOfflineDisabled } from '../../../core/hooks/useOfflineDisabled';
+import { BOOKINGS_QUERY_KEY } from '../../../core/queries/bookingHooks';
import { TICKETS_QUERY_KEY } from '../../../core/queries/ticketHooks';
import { split } from '../../../utils/reducers';
import { ServiceCard } from '../components/ServiceCard';
@@ -99,16 +100,19 @@ export const ServicesScreen = () => {
disabled: isOffline && peopleSearched?.length === 0,
linkTo: { screen: 'Contacts' },
},
- {
- id: 'guides',
- name: t('guidesScreen.title'),
- icon: faSignsPost,
- disabled: true,
- },
{
id: 'bookings',
name: t('bookingsScreen.title'),
icon: faPersonCirclePlus,
+ disabled:
+ isOffline &&
+ queryClient.getQueryData(BOOKINGS_QUERY_KEY) === undefined,
+ linkTo: { screen: 'Bookings' },
+ },
+ {
+ id: 'guides',
+ name: t('guidesScreen.title'),
+ icon: faSignsPost,
disabled: true,
},
{
diff --git a/src/features/teaching/components/ExamListItem.tsx b/src/features/teaching/components/ExamListItem.tsx
index 1533c02b..ad4ef8af 100644
--- a/src/features/teaching/components/ExamListItem.tsx
+++ b/src/features/teaching/components/ExamListItem.tsx
@@ -102,7 +102,7 @@ export const ExamListItem = ({
ellipsizeMode="tail"
style={{ flexShrink: 1 }}
>
- {exam.classrooms}
+ {exam.places?.map(place => place.name).join(', ')}
diff --git a/src/features/teaching/screens/CourseLecturesScreen.tsx b/src/features/teaching/screens/CourseLecturesScreen.tsx
index 9ea593fd..f0568553 100644
--- a/src/features/teaching/screens/CourseLecturesScreen.tsx
+++ b/src/features/teaching/screens/CourseLecturesScreen.tsx
@@ -37,6 +37,7 @@ import {
CourseLecture,
CourseLectureSection,
} from '../types/CourseLectureSections';
+import { isRecordedVC, isVideoLecture } from '../utils/lectures';
export const CourseLecturesScreen = () => {
const { t } = useTranslation();
@@ -193,21 +194,26 @@ export const CourseLectureListItem = ({
const { fontSizes } = useTheme();
const { t } = useTranslation();
+ let duration = null;
+ if (isRecordedVC(lecture) || isVideoLecture(lecture)) {
+ duration = lecture.duration;
+ }
+
return (
!!i)
.join(' - ')}
accessibilityLabel={[
lecture.title,
- lecture.duration
- .replace('m', t('common.minutes'))
- .replace('h', t('common.hours')),
+ duration
+ ?.replace('m', t('common.minutes'))
+ ?.replace('h', t('common.hours')),
teacher && `${teacher.firstName} ${teacher.lastName}`,
]
.filter(i => !!i)
diff --git a/src/features/teaching/screens/CourseVirtualClassroomScreen.tsx b/src/features/teaching/screens/CourseVirtualClassroomScreen.tsx
index ac4fac1c..da7e9a77 100644
--- a/src/features/teaching/screens/CourseVirtualClassroomScreen.tsx
+++ b/src/features/teaching/screens/CourseVirtualClassroomScreen.tsx
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { SafeAreaView, ScrollView } from 'react-native';
+import { SafeAreaView, ScrollView, View } from 'react-native';
import { OverviewList } from '@lib/ui/components/OverviewList';
import { PersonListItem } from '@lib/ui/components/PersonListItem';
@@ -15,6 +15,7 @@ import { useGetPerson } from '../../../core/queries/peopleHooks';
import { GlobalStyles } from '../../../core/styles/globalStyles';
import { formatDateWithTimeIfNotNull } from '../../../utils/dates';
import { TeachingStackParamList } from '../components/TeachingNavigator';
+import { isLiveVC, isRecordedVC } from '../utils/lectures';
type Props = NativeStackScreenProps<
TeachingStackParamList,
@@ -43,12 +44,17 @@ export const CourseVirtualClassroomScreen = ({ route }: Props) => {
contentContainerStyle={GlobalStyles.fillHeight}
>
- {lecture?.type === 'recording' && lecture?.videoUrl && (
+ {lecture && isRecordedVC(lecture) && lecture.videoUrl && (
)}
+ {lecture && isLiveVC(lecture) && (
+
+ // TODO handle live VC
+ //
+ )}
{
}
}, [navigation, routes, t]);
+ const classrooms = exam?.places?.map(p => p.name).join(', ') ?? '-';
+
const examAccessibilityLabel = useMemo(() => {
if (!exam || !teacherQuery.data) return;
@@ -78,15 +77,13 @@ export const ExamScreen = ({ route, navigation }: Props) => {
}
}
- const classrooms =
- exam?.classrooms && exam?.classrooms !== '-'
- ? `${t('examScreen.location')}: ${exam?.classrooms}`
- : '';
+ const accessibleClassrooms =
+ classrooms !== '-' ? `${t('examScreen.location')}: ${classrooms}` : '';
const teacher = `${t('common.teacher')}: ${teacherQuery.data.firstName} ${
teacherQuery.data.lastName
}`;
- return `${exam.courseName}. ${accessibleDateTime}. ${classrooms} ${teacher}`;
+ return `${exam.courseName}. ${accessibleDateTime}. ${accessibleClassrooms} ${teacher}`;
}, [exam, t, teacherQuery]);
return (
@@ -115,34 +112,21 @@ export const ExamScreen = ({ route, navigation }: Props) => {
{exam?.status && }
-
-
-
-
- {exam?.examStartsAt
- ? formatReadableDate(exam.examStartsAt)
- : t('common.dateToBeDefined')}
-
-
-
-
-
- {exam?.examStartsAt
- ? `${formatTime(exam.examStartsAt)} - ${formatTime(
- exam.examEndsAt!,
- )}`
- : t('common.timeToBeDefined')}
-
-
-
+
@@ -150,11 +134,9 @@ export const ExamScreen = ({ route, navigation }: Props) => {
leadingItem={
}
- title={exam?.classrooms ?? '-'}
+ title={classrooms}
accessibilityLabel={`${t('examScreen.location')}: ${
- exam?.classrooms === '-'
- ? t('examScreen.noClassroom')
- : exam?.classrooms
+ classrooms !== '-' ? classrooms : t('examScreen.noClassroom')
}`}
subtitle={t('examScreen.location')}
/>
diff --git a/src/features/teaching/screens/TeachingScreen.tsx b/src/features/teaching/screens/TeachingScreen.tsx
index 117b4faa..ee790cab 100644
--- a/src/features/teaching/screens/TeachingScreen.tsx
+++ b/src/features/teaching/screens/TeachingScreen.tsx
@@ -69,7 +69,7 @@ export const TeachingScreen = ({ navigation }: Props) => {
const hiddenNonModuleCourses: string[] = [];
Object.keys(coursePreferences).forEach((key: string) => {
- if (coursePreferences[+key].isHidden) {
+ if (coursePreferences[+key]?.isHidden) {
const hiddenCourse = coursesQuery.data?.find(c => c.id === +key);
if (hiddenCourse && !hiddenCourse.isModule)
hiddenNonModuleCourses.push(hiddenCourse.shortcode);
diff --git a/src/features/teaching/utils/lectures.ts b/src/features/teaching/utils/lectures.ts
new file mode 100644
index 00000000..3bf48a1f
--- /dev/null
+++ b/src/features/teaching/utils/lectures.ts
@@ -0,0 +1,19 @@
+import { VideoLecture } from '@polito/api-client';
+import { instanceOfVideoLecture } from '@polito/api-client/models/VideoLecture';
+import {
+ VirtualClassroom,
+ instanceOfVirtualClassroom,
+} from '@polito/api-client/models/VirtualClassroom';
+import {
+ VirtualClassroomLive,
+ instanceOfVirtualClassroomLive,
+} from '@polito/api-client/models/VirtualClassroomLive';
+
+export const isLiveVC = (l: object): l is VirtualClassroomLive =>
+ instanceOfVirtualClassroomLive(l);
+
+export const isRecordedVC = (l: object): l is VirtualClassroom =>
+ instanceOfVirtualClassroom(l);
+
+export const isVideoLecture = (l: object): l is VideoLecture =>
+ instanceOfVideoLecture(l);